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