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