1 ///
2 module glui.space;
3 
4 import raylib;
5 
6 import std.math;
7 import std.range;
8 import std.string;
9 import std.traits;
10 import std.algorithm;
11 
12 import glui.node;
13 import glui.style;
14 import glui.utils;
15 import glui.children;
16 
17 
18 @safe:
19 
20 
21 /// Make a new vertical space.
22 alias vspace = simpleConstructor!GluiSpace;
23 
24 /// Make a new horizontal space.
25 alias hspace = simpleConstructor!(GluiSpace, (a) {
26 
27     a.directionHorizontal = true;
28 
29 });
30 
31 /// This is a space, a basic container for other nodes.
32 ///
33 /// Space only acts as a container and doesn't implement styles and doesn't take focus. It can be very useful to build
34 /// overlaying nodes, eg. with `GluiOnionFrame`.
35 class GluiSpace : GluiNode {
36 
37     mixin DefineStyles;
38 
39     /// Children of this frame.
40     Children children;
41 
42     /// Defines in what directions children of this frame should be placed.
43     ///
44     /// If true, children are placed horizontally, if false, vertically.
45     bool horizontal;
46 
47     alias directionHorizontal = horizontal;
48 
49     private {
50 
51         /// Denominator for content sizing.
52         uint denominator;
53 
54         /// Space reserved for shrinking elements.
55         uint reservedSpace;
56 
57     }
58 
59     // Generate constructors
60     static foreach (index; 0 .. BasicNodeParamLength) {
61 
62         this(BasicNodeParam!index params, GluiNode[] nodes...) {
63 
64             super(params);
65             this.children ~= nodes;
66 
67         }
68 
69     }
70 
71     /// Add children.
72     pragma(inline, true)
73     void opOpAssign(string operator : "~", T)(T nodes) {
74 
75         children ~= nodes;
76 
77     }
78 
79     protected override void resizeImpl(Vector2 available) {
80 
81         import std.algorithm : max, map, fold;
82 
83         // Reset size
84         minSize = Vector2(0, 0);
85         reservedSpace = 0;
86         denominator = 0;
87 
88         // Ignore the rest if there's no children
89         if (!children.length) return;
90 
91         Vector2 maxExpandSize;
92 
93         // Collect expanding children in a separate array
94         GluiNode[] expandChildren;
95         foreach (child; children) {
96 
97             // This node expands and isn't hidden
98             if (child.layout.expand && !child.isHidden) {
99 
100                 // Make it happen later
101                 expandChildren ~= child;
102 
103                 // Add to the denominator
104                 denominator += child.layout.expand;
105 
106             }
107 
108             // Check non-expand nodes now
109             else {
110 
111                 child.resize(tree, theme, childSpace(child, available));
112                 minSize = childPosition(child.minSize, minSize);
113 
114                 // Reserve space for this node
115                 reservedSpace += directionHorizontal
116                     ? cast(uint) child.minSize.x
117                     : cast(uint) child.minSize.y;
118 
119             }
120 
121         }
122 
123         // Calculate the size of expanding children last
124         foreach (child; expandChildren) {
125 
126             // Resize the child
127             child.resize(tree, theme, childSpace(child, available));
128 
129             const childSize = child.minSize;
130             const childExpand = child.layout.expand;
131 
132             const segmentSize = horizontal
133                 ? Vector2(childSize.x / childExpand, childSize.y)
134                 : Vector2(childSize.x, childSize.y / childExpand);
135 
136             // Reserve expand space
137             maxExpandSize.x = max(maxExpandSize.x, segmentSize.x);
138             maxExpandSize.y = max(maxExpandSize.y, segmentSize.y);
139 
140         }
141 
142         const expandSize = horizontal
143             ? Vector2(maxExpandSize.x * denominator, maxExpandSize.y)
144             : Vector2(maxExpandSize.x, maxExpandSize.y * denominator);
145 
146         // Add the expand space
147         minSize = childPosition(expandSize, minSize);
148 
149     }
150 
151     protected override void drawImpl(Rectangle, Rectangle area) {
152 
153         auto position = Vector2(area.x, area.y);
154 
155         drawChildren((child) {
156 
157             // Get params
158             const size = childSpace(child, Vector2(area.width, area.height));
159             const rect = Rectangle(
160                 position.x, position.y,
161                 size.x, size.y
162             );
163 
164             // Draw the child
165             child.draw(rect);
166 
167             // Offset position
168             if (directionHorizontal) position.x += cast(int) size.x;
169             else position.y += cast(int) size.y;
170 
171         });
172 
173     }
174 
175     /// Iterate over every child and perform the painting function. Will automatically remove nodes queued for removal.
176     protected void drawChildren(void delegate(GluiNode) @safe painter) {
177 
178         GluiNode[] leftovers;
179 
180         children.lock();
181         scope (exit) children.unlock();
182 
183         // Draw each child and get rid of removed children
184         auto range = children[]
185 
186             // Check if the node is queued for removal
187             .filter!((node) {
188                 const status = node.toRemove;
189                 node.toRemove = false;
190                 return !status;
191             })
192 
193             // Draw the node
194             .tee!((node) => painter(node));
195 
196         // Do what we ought to do
197         () @trusted {
198 
199             // Process the children and move them back to the original array
200             auto leftovers = range.moveAll(children.forceMutable);
201 
202             // Adjust the array size
203             children.forceMutable.length -= leftovers.length;
204 
205         }();
206 
207     }
208 
209     protected override bool hoveredImpl(Rectangle, Vector2) const {
210 
211         return false;
212 
213     }
214 
215     protected override const(Style) pickStyle() const {
216 
217         return null;
218 
219     }
220 
221     /// Params:
222     ///     child     = Child size to add.
223     ///     previous  = Previous position.
224     private Vector2 childPosition(Vector2 child, Vector2 previous) const {
225 
226         import std.algorithm : max;
227 
228         // Horizontal
229         if (directionHorizontal) {
230 
231             return Vector2(
232                 previous.x + child.x,
233                 max(minSize.y, child.y),
234             );
235 
236         }
237 
238         // Vertical
239         else return Vector2(
240             max(minSize.x, child.x),
241             previous.y + child.y,
242         );
243 
244     }
245 
246     /// Get space for a child.
247     /// Params:
248     ///     child     = Child to place
249     ///     available = Available space
250     private Vector2 childSpace(const GluiNode child, Vector2 available) const
251     in(
252         child.isHidden || child.layout.expand <= denominator,
253         format!"Nodes %s/%s sizes are out of date, call updateSize after updating the tree or layout (%s/%s)"(
254             typeid(this), typeid(child), child.layout.expand, denominator,
255         )
256     )
257     out(
258         r; [r.tupleof].all!isFinite,
259         format!"space: child %s given invalid size %s. available = %s, expand = %s, denominator = %s, reserved = %s"(
260             typeid(child), r, available, child.layout.expand, denominator, reservedSpace
261         )
262     )
263     do {
264 
265         // Hidden, give it no space
266         if (child.isHidden) return Vector2();
267 
268         // Horizontal
269         if (directionHorizontal) {
270 
271             const avail = (available.x - reservedSpace);
272 
273             return Vector2(
274                 child.layout.expand
275                     ? avail * child.layout.expand / denominator
276                     : child.minSize.x,
277                 available.y,
278             );
279 
280         }
281 
282         // Vertical
283         else {
284 
285             const avail = (available.y - reservedSpace);
286 
287             return Vector2(
288                 available.x,
289                 child.layout.expand
290                     ? avail * child.layout.expand / denominator
291                     : child.minSize.y,
292             );
293 
294         }
295 
296     }
297 
298 }