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 @safe:
15 
16 
17 deprecated("`Grid` and `grid` were renamed to `GridFrame` and `gridFrame` respectively. To be removed in 0.8.0.") {
18 
19     alias grid = simpleConstructor!GridFrame;
20     alias Grid = GridFrame;
21 
22 }
23 
24 alias gridFrame = simpleConstructor!GridFrame;
25 alias gridRow = simpleConstructor!GridRow;
26 
27 // TODO rename segments to columns?
28 
29 /// Segments is used to set the number of columns spanned by a grid item. When applied to a grid, it sets the number of
30 /// columns the grid will have.
31 struct Segments {
32 
33     /// Number of columns used by a grid item.
34     uint amount = 1;
35 
36     /// Set the number of columns present in a grid.
37     void apply(GridFrame grid) {
38 
39         grid.segmentCount = amount;
40 
41     }
42 
43     /// Set the number of columns used by this node.
44     void apply(Node node) {
45 
46         node.layout.expand = amount;
47 
48     }
49 
50 }
51 
52 /// ditto
53 Segments segments(uint columns) {
54 
55     return Segments(columns);
56 
57 }
58 
59 /// ditto
60 Segments segments(uint columns)() {
61 
62     return Segments(columns);
63 
64 }
65 
66 /// The GridFrame node will align its children in a 2D grid.
67 class GridFrame : Frame {
68 
69     size_t segmentCount;
70 
71     private {
72 
73         /// Sizes for each segment.
74         int[] _segmentSizes;
75 
76         /// Last grid width given.
77         float lastWidth;
78 
79     }
80 
81     this(Ts...)(Ts children) {
82 
83         this.children.length = children.length;
84 
85         // Check the other arguments
86         static foreach (i, arg; children) {{
87 
88             // Grid row (via array)
89             static if (is(typeof(arg) : U[], U)) {
90 
91                 this.children[i] = gridRow(arg);
92 
93             }
94 
95             // Other stuff
96             else {
97 
98                 this.children[i] = arg;
99 
100             }
101 
102         }}
103 
104     }
105 
106     /// Add a new row to this grid.
107     void addRow(Ts...)(Ts content) {
108 
109         children ~= gridRow(content);
110 
111     }
112 
113     /// Magic to extract return value of extractParams at compile time.
114     private struct Number(size_t num) {
115 
116         enum value = num;
117 
118     }
119 
120     /// Returns:
121     ///     An array of numbers indicating the width of each segment in the grid.
122     const(int)[] segmentSizes() const {
123 
124         return _segmentSizes;
125 
126     }
127 
128     /// Evaluate special parameters and get the index of the first non-special parameter (not Segments, Layout nor
129     /// Theme).
130     /// Returns: An instance of `Number` with said index as parameter.
131     private auto extractParams(Args...)(Args args) {
132 
133         enum maxInitialArgs = min(args.length, 3);
134 
135         static foreach (i, arg; args[0..maxInitialArgs]) {
136 
137             // Complete; wait to the end
138             static if (__traits(compiles, endIndex)) { }
139 
140             // Segment count
141             else static if (is(typeof(arg) : Segments)) {
142 
143                 segmentCount = arg.expand;
144 
145             }
146 
147             // Layout
148             else static if (is(typeof(arg) : Layout)) {
149 
150                 layout = arg;
151 
152             }
153 
154             // Theme
155             else static if (is(typeof(arg) : Theme)) {
156 
157                 theme = arg;
158 
159             }
160 
161             // Mark this as the end
162             else enum endIndex = i;
163 
164         }
165 
166         static if (!__traits(compiles, endIndex)) {
167 
168             enum endIndex = maxInitialArgs;
169 
170         }
171 
172         return Number!endIndex();
173 
174     }
175 
176     override protected void resizeImpl(Vector2 space) {
177 
178         import std.numeric;
179 
180         // Need to recalculate segments
181         if (segmentCount == 0) {
182 
183             // Increase segment count
184             segmentCount = 1;
185 
186             // Check children
187             foreach (child; children) {
188 
189                 // Only count rows
190                 if (auto row = cast(GridRow) child) {
191 
192                     // Set self as parent
193                     row.parent = this;
194 
195                     // Recalculate the segments needed by the row
196                     row.calculateSegments();
197 
198                     // Set the segment count to the lowest common multiple of the current segment count and the cell
199                     // count of this row
200                     segmentCount = lcm(segmentCount, row.segmentCount);
201 
202                 }
203 
204             }
205 
206         }
207 
208         else {
209 
210             foreach (child; children) {
211 
212                 // Assign self as parent to all rows
213                 if (auto row = cast(GridRow) child) {
214                     row.parent = this;
215                 }
216 
217             }
218 
219         }
220 
221         // Reserve the segments
222         _segmentSizes = new int[segmentCount];
223 
224         // Resize the children
225         super.resizeImpl(space);
226 
227         // Reset width
228         lastWidth = 0;
229 
230     }
231 
232     override void drawImpl(Rectangle outer, Rectangle inner) {
233 
234         // TODO WHY is this done here and not in resizeImpl?
235         void expand(Node child) {
236 
237             // Given more grid space than we allocated
238             if (lastWidth >= inner.width + 1) return;
239 
240             // Only proceed if the given node is a row
241             if (!cast(GridRow) child) return;
242 
243             // Update the width
244             lastWidth = inner.width;
245 
246             // Get margin for the row
247             const rowMargin = child.style.totalMargin;
248             // Note: We're assuming all rows have the same margin. This might not hold true with the introduction of
249             // tags.
250 
251             // Expand the segments to match box size
252             redistributeSpace(_segmentSizes, inner.width - rowMargin.sideLeft - rowMargin.sideRight);
253 
254         }
255 
256         // Draw the background
257         pickStyle.drawBackground(tree.io, canvasIO, outer);
258 
259         // Get the position
260         auto position = inner.y;
261 
262         // Draw the rows
263         foreach (child; filterChildren) {
264 
265             // Get params
266             const rect = Rectangle(
267                 inner.x, position,
268                 inner.width, child.minSize.y
269             );
270 
271             // Try to expand grid segments
272             expand(child);
273 
274             // Draw the child
275             drawChild(child, rect);
276 
277             // Offset position
278             position += child.minSize.y + style.gap.sideY;
279 
280         }
281 
282     }
283 
284 }
285 
286 /// A single row in a `Grid`.
287 class GridRow : Frame {
288 
289     GridFrame parent;
290     size_t segmentCount;
291 
292     /// Params:
293     ///     nodes = Children to be placed in the row.
294     this(Ts...)(Ts nodes) {
295 
296         super(nodes);
297         this.layout.nodeAlign = NodeAlign.fill;
298         this.directionHorizontal = true;
299 
300     }
301 
302     void calculateSegments() {
303 
304         segmentCount = 0;
305 
306         // Count segments used by each child
307         foreach (child; children) {
308 
309             segmentCount += either(child.layout.expand, 1);
310 
311         }
312 
313     }
314 
315     override void resizeImpl(Vector2 space) {
316 
317         use(canvasIO);
318 
319         // Reset the size
320         minSize = Vector2();
321 
322         // Empty row; do nothing
323         if (children.length == 0) return;
324 
325         // No segments calculated, run now
326         if (segmentCount == 0) {
327 
328             calculateSegments();
329 
330         }
331 
332         size_t segment;
333 
334         // Resize the children
335         foreach (i, child; children) {
336 
337             const segments = either(child.layout.expand, 1);
338             const gapSpace = max(
339                 0,
340                 style.gap.sideX * (cast(ptrdiff_t) children.length - 1)
341             );
342             const childSpace = Vector2(
343                 space.x * segments / segmentCount - gapSpace,
344                 minSize.y,
345             );
346 
347             scope (exit) segment += segments;
348 
349             // Resize the child
350             resizeChild(child, childSpace);
351 
352             auto range = parent._segmentSizes[segment..segment+segments];
353 
354             // Include the gap for all, but the first child
355             const gap = i == 0 ? 0 : style.gap.sideX;
356 
357             // Second step: Expand the segments to give some space for the child
358             minSize.x += range.redistributeSpace(child.minSize.x + gap);
359 
360             // Increase vertical space, if needed
361             if (child.minSize.y > minSize.y) {
362 
363                 minSize.y = child.minSize.y;
364 
365             }
366 
367         }
368 
369     }
370 
371     override protected void drawImpl(Rectangle outer, Rectangle inner) {
372 
373         size_t segment;
374 
375         pickStyle.drawBackground(tree.io, canvasIO, outer);
376 
377         /// Child position.
378         auto position = Vector2(inner.x, inner.y);
379 
380         foreach (i, child; filterChildren) {
381 
382             const segments = either(child.layout.expand, 1);
383             const gap = i == 0 ? 0 : style.gap.sideX;
384             const width = parent.segmentSizes[segment..segment+segments].sum;
385 
386             // Draw the child
387             drawChild(child, Rectangle(
388                 position.x + gap, position.y,
389                 width - gap, inner.height,
390             ));
391 
392             // Proceed to the next segment
393             segment += segments;
394             position.x += width;
395 
396         }
397 
398     }
399 
400 }
401 
402 /// Redistribute space for the given row spacing range. It will increase the size of as many cells as possible as long
403 /// as they can stay even.
404 ///
405 /// Does nothing if amount of space was reduced.
406 ///
407 /// Params:
408 ///     range = Range to work on and modify.
409 ///     space = New amount of space to apply. The resulting sum of range items will be equal or greater (if it was
410 ///         already greater) to this number.
411 /// Returns:
412 ///     Newly acquired amount of space, the resulting sum of range size.
413 private ElementType!Range redistributeSpace(Range, Numeric)(ref Range range, Numeric space, string caller = __FUNCTION__) {
414 
415     import std.math;
416 
417     alias RangeNumeric = ElementType!Range;
418 
419     // Get a sorted copy of the range
420     auto sortedCopy = range.dup.sort!"a > b";
421 
422     // Find smallest item & current size of the range
423     RangeNumeric currentSize;
424     RangeNumeric smallestItem;
425 
426     // Check current data from the range
427     foreach (item; sortedCopy.save) {
428 
429         currentSize += item;
430         smallestItem = item;
431 
432     }
433 
434     /// Extra space to give
435     auto extra = cast(double) space - currentSize;
436 
437     // Do nothing if there's no extra space
438     if (extra < 0 || extra.isClose(0)) return currentSize;
439 
440     /// Space to give per segment
441     RangeNumeric perElement;
442 
443     // Check all segments
444     foreach (i, segment; sortedCopy.enumerate) {
445 
446         // Split the available over all remaining segments
447         perElement = smallestItem + cast(RangeNumeric) ceil(extra / (range.length - i));
448 
449         // Skip segments if the resulting size isn't big enough
450         if (perElement > segment) break;
451 
452     }
453 
454     RangeNumeric total;
455 
456     // Assign the size
457     foreach (ref item; range)  {
458 
459         // Grow this one
460         item = max(item, perElement);
461         total += item;
462 
463     }
464 
465     return total;
466 
467 }