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("Renamed to `gridFrame`") alias grid = simpleConstructor!Grid;
19 alias gridFrame = grid;
20 alias gridRow = simpleConstructor!GridRow;
21 
22 // TODO rename segments to columns?
23 
24 /// Segments is used to set the number of columns spanned by a grid item. When applied to a grid, it sets the number of
25 /// columns the grid will have.
26 struct Segments {
27 
28     /// Number of columns used by a grid item.
29     uint amount = 1;
30 
31     /// Set the number of columns present in a grid.
32     void apply(Grid grid) {
33 
34         grid.segmentCount = amount;
35 
36     }
37 
38     /// Set the number of columns used by this node.
39     void apply(Node node) {
40 
41         node.layout.expand = amount;
42 
43     }
44 
45 }
46 
47 /// ditto
48 Segments segments(uint columns) {
49 
50     return Segments(columns);
51 
52 }
53 
54 /// ditto
55 Segments segments(uint columns)() {
56 
57     return Segments(columns);
58 
59 }
60 
61 deprecated("Renamed to `GridFrame`") alias Grid = GridFrame;
62 
63 /// The GridFrame node will align its children in a 2D grid.
64 class GridFrame : Frame {
65 
66     size_t segmentCount;
67 
68     private {
69 
70         /// Sizes for each segment.
71         int[] segmentSizes;
72 
73         /// Last grid width given.
74         float lastWidth;
75 
76     }
77 
78     this(Ts...)(Ts children) {
79 
80         this.children.length = children.length;
81 
82         // Check the other arguments
83         static foreach (i, arg; children) {{
84 
85             // Grid row (via array)
86             static if (is(typeof(arg) : U[], U)) {
87 
88                 this.children[i] = gridRow(this, arg);
89 
90             }
91 
92             // Other stuff
93             else {
94 
95                 this.children[i] = arg;
96 
97             }
98 
99         }}
100 
101     }
102 
103     unittest {
104 
105         import std.math;
106         import std.array;
107         import std.typecons;
108         import fluid.label;
109 
110         auto io = new HeadlessBackend;
111         auto root = grid(
112             .Theme.init.makeTheme!q{
113                 Label.styleAdd!q{
114                     textColor = color!"000";
115                 };
116             },
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(this, 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                     // Recalculate the segments needed by the row
270                     row.calculateSegments();
271 
272                     // Set the segment count to the lowest common multiple of the current segment count and the cell
273                     // count of this row
274                     segmentCount = lcm(segmentCount, row.segmentCount);
275 
276                 }
277 
278             }
279 
280         }
281 
282         // Reserve the segments
283         segmentSizes = new int[segmentCount];
284 
285         // Resize the children
286         super.resizeImpl(space);
287 
288         // Reset width
289         lastWidth = 0;
290 
291     }
292 
293     override void drawImpl(Rectangle outer, Rectangle inner) {
294 
295         // TODO WHY is this done here and not in resizeImpl?
296         void expand(Node child) {
297 
298             // Given more grid space than we allocated
299             if (lastWidth >= inner.width + 1) return;
300 
301             // Only proceed if the given node is a row
302             if (!cast(GridRow) child) return;
303 
304             // Update the width
305             lastWidth = inner.width;
306 
307             // Get margin for the row
308             const rowMargin = child.style.totalMargin;
309             // Note: We're assuming all rows have the same margin. This might not hold true with the introduction of
310             // tags.
311 
312             // Expand the segments to match box size
313             redistributeSpace(segmentSizes, inner.width - rowMargin.sideLeft - rowMargin.sideRight);
314 
315         }
316 
317         // Draw the background
318         pickStyle.drawBackground(tree.io, outer);
319 
320         // Get the position
321         auto position = inner.y;
322 
323         // Draw the rows
324         foreach (child; filterChildren) {
325 
326             // Get params
327             const rect = Rectangle(
328                 inner.x, position,
329                 inner.width, child.minSize.y
330             );
331 
332             // Try to expand grid segments
333             expand(child);
334 
335             // Draw the child
336             child.draw(rect);
337 
338             // Offset position
339             position += child.minSize.y + style.gap;
340 
341         }
342 
343     }
344 
345     unittest {
346 
347         import fluid.label;
348 
349         // Nodes are to span segments in order:
350         // 1. One label to span 6 segments
351         // 2. Each 3 segments
352         // 3. Each 2 segments
353         auto g = grid(
354             [ label("") ],
355             [ label(""), label("") ],
356             [ label(""), label(""), label("") ],
357         );
358 
359         g.backend = new HeadlessBackend;
360         g.draw();
361 
362         assert(g.segmentCount == 6);
363 
364     }
365 
366 }
367 
368 /// A single row in a `Grid`.
369 class GridRow : Frame {
370 
371     Grid parent;
372     size_t segmentCount;
373 
374     /// Params:
375     ///     params = Standard Fluid constructor parameters.
376     ///     parent = Grid this row will be placed in.
377     ///     nodes = Children to be placed in the row.
378     this(T...)(Grid parent, T nodes) {
379 
380         super(nodes);
381         this.layout.nodeAlign = NodeAlign.fill;
382         this.parent = parent;
383         this.directionHorizontal = true;
384 
385     }
386 
387     void calculateSegments() {
388 
389         segmentCount = 0;
390 
391         // Count segments used by each child
392         foreach (child; children) {
393 
394             segmentCount += either(child.layout.expand, 1);
395 
396         }
397 
398     }
399 
400     override void resizeImpl(Vector2 space) {
401 
402         // Reset the size
403         minSize = Vector2();
404 
405         // Empty row; do nothing
406         if (children.length == 0) return;
407 
408         // No segments calculated, run now
409         if (segmentCount == 0) {
410 
411             calculateSegments();
412 
413         }
414 
415         size_t segment;
416 
417         // Resize the children
418         foreach (child; children) {
419 
420             const segments = either(child.layout.expand, 1);
421             const childSpace = Vector2(
422                 space.x * segments / segmentCount,
423                 minSize.y,
424             );
425 
426             scope (exit) segment += segments;
427 
428             // Resize the child
429             child.resize(tree, theme, childSpace);
430 
431             auto range = parent.segmentSizes[segment..segment+segments];
432 
433             // Second step: Expand the segments to give some space for the child
434             minSize.x += range.redistributeSpace(child.minSize.x);
435 
436             // Increase vertical space, if needed
437             if (child.minSize.y > minSize.y) {
438 
439                 minSize.y = child.minSize.y;
440 
441             }
442 
443         }
444 
445     }
446 
447     override protected void drawImpl(Rectangle outer, Rectangle inner) {
448 
449         size_t segment;
450 
451         pickStyle.drawBackground(tree.io, outer);
452 
453         /// Child position.
454         auto position = Vector2(inner.x, inner.y);
455 
456         foreach (child; filterChildren) {
457 
458             const segments = either(child.layout.expand, 1);
459             const width = parent.segmentSizes[segment..segment+segments].sum;
460 
461             // Draw the child
462             child.draw(Rectangle(
463                 position.x, position.y,
464                 width, inner.height,
465             ));
466 
467             // Proceed to the next segment
468             segment += segments;
469             position.x += width;
470 
471         }
472 
473     }
474 
475 }
476 
477 /// Redistribute space for the given row spacing range. It will increase the size of as many cells as possible as long
478 /// as they can stay even.
479 ///
480 /// Does nothing if amount of space was reduced.
481 ///
482 /// Params:
483 ///     range = Range to work on and modify.
484 ///     space = New amount of space to apply. The resulting sum of range items will be equal or greater (if it was
485 ///         already greater) to this number.
486 /// Returns:
487 ///     Newly acquired amount of space, the resulting sum of range size.
488 private ElementType!Range redistributeSpace(Range, Numeric)(ref Range range, Numeric space, string caller = __FUNCTION__) {
489 
490     import std.math;
491 
492     alias RangeNumeric = ElementType!Range;
493 
494     // Get a sorted copy of the range
495     auto sortedCopy = range.dup.sort!"a > b";
496 
497     // Find smallest item & current size of the range
498     RangeNumeric currentSize;
499     RangeNumeric smallestItem;
500 
501     // Check current data from the range
502     foreach (item; sortedCopy.save) {
503 
504         currentSize += item;
505         smallestItem = item;
506 
507     }
508 
509     /// Extra space to give
510     auto extra = cast(double) space - currentSize;
511 
512     // Do nothing if there's no extra space
513     if (extra < 0 || extra.isClose(0)) return currentSize;
514 
515     /// Space to give per segment
516     RangeNumeric perElement;
517 
518     // Check all segments
519     foreach (i, segment; sortedCopy.enumerate) {
520 
521         // Split the available over all remaining segments
522         perElement = smallestItem + cast(RangeNumeric) ceil(extra / (range.length - i));
523 
524         // Skip segments if the resulting size isn't big enough
525         if (perElement > segment) break;
526 
527     }
528 
529     RangeNumeric total;
530 
531     // Assign the size
532     foreach (ref item; range)  {
533 
534         // Grow this one
535         item = max(item, perElement);
536         total += item;
537 
538     }
539 
540     return total;
541 
542 }