1 ///
2 module fluid.space;
3 
4 import std.math;
5 import std.range;
6 import std.string;
7 import std.traits;
8 import std.algorithm;
9 
10 import fluid.node;
11 import fluid.style;
12 import fluid.utils;
13 import fluid.backend;
14 import fluid.children;
15 
16 
17 @safe:
18 
19 
20 /// This is a space, a basic container for other nodes.
21 ///
22 /// Nodes are laid in a column (`vframe`) or in a row (`hframe`).
23 ///
24 /// Space only acts as a container and doesn't implement styles and doesn't take focus. It's very useful as a helper for
25 /// building layout, while `Frame` remains to provide styling.
26 alias vspace = simpleConstructor!Space;
27 
28 /// ditto
29 alias hspace = simpleConstructor!(Space, (a) {
30 
31     a.directionHorizontal = true;
32 
33 });
34 
35 /// ditto
36 class Space : Node {
37 
38     public {
39 
40         /// Children of this frame.
41         Children children;
42 
43         /// Defines in what directions children of this frame should be placed.
44         ///
45         /// If true, children are placed horizontally, if false, vertically.
46         bool isHorizontal;
47 
48         alias horizontal = isHorizontal;
49         alias directionHorizontal = horizontal;
50 
51     }
52 
53     private {
54 
55         /// Denominator for content sizing.
56         uint denominator;
57 
58         /// Space reserved for shrinking elements.
59         float reservedSpace;
60 
61     }
62 
63     /// Create the space and fill it with given nodes.
64     this(Node[] nodes...) {
65 
66         this.children ~= nodes;
67 
68     }
69 
70     /// Create the space using nodes from the given range.
71     this(Range)(Range range)
72     if (isInputRange!Range)
73     do {
74 
75         this.children ~= range;
76 
77     }
78 
79     /// Add children.
80     pragma(inline, true)
81     void opOpAssign(string operator : "~", T)(T nodes) {
82 
83         children ~= nodes;
84 
85     }
86 
87     protected override void resizeImpl(Vector2 available) {
88 
89         import std.algorithm : max, map, fold;
90 
91         // Now that we're recalculating the layout, we can remove the dirty flag from children
92         children.clearDirty;
93 
94         // Reset size
95         minSize = Vector2(0, 0);
96         reservedSpace = 0;
97         denominator = 0;
98 
99         // Ignore the rest if there's no children
100         if (!children.length) return;
101 
102         Vector2 maxExpandSize;
103 
104         // Collect expanding children in a separate array
105         Node[] expandChildren;
106         size_t visibleChildren;
107         foreach (child; children) {
108 
109             visibleChildren += !child.isHidden;
110 
111             // This node expands and isn't hidden
112             if (child.layout.expand && !child.isHidden) {
113 
114                 // Make it happen later
115                 expandChildren ~= child;
116 
117                 // Add to the denominator
118                 denominator += child.layout.expand;
119 
120             }
121 
122             // Check non-expand nodes now
123             else {
124 
125                 resizeChild(child, childSpace(child, available, false));
126                 minSize = addSize(child.minSize, minSize);
127 
128                 // Reserve space for this node
129                 reservedSpace += directionHorizontal
130                     ? child.minSize.x
131                     : child.minSize.y;
132 
133             }
134 
135         }
136 
137         const gapSpace 
138             = visibleChildren == 0 ? 0
139             : isHorizontal ? style.gap.sideX * (visibleChildren - 1u)
140             :                style.gap.sideY * (visibleChildren - 1u);
141 
142         // Reserve space for gaps
143         reservedSpace += gapSpace;
144 
145         if (isHorizontal)
146             minSize.x += gapSpace;
147         else
148             minSize.y += gapSpace;
149 
150         // Calculate the size of expanding children last
151         foreach (child; expandChildren) {
152 
153             // Resize the child
154             resizeChild(child, childSpace(child, available, false));
155 
156             const childSize = child.minSize;
157             const childExpand = child.layout.expand;
158 
159             const segmentSize = horizontal
160                 ? Vector2(childSize.x / childExpand, childSize.y)
161                 : Vector2(childSize.x, childSize.y / childExpand);
162 
163             // Reserve expand space
164             maxExpandSize.x = max(maxExpandSize.x, segmentSize.x);
165             maxExpandSize.y = max(maxExpandSize.y, segmentSize.y);
166 
167         }
168 
169         const expandSize = horizontal
170             ? Vector2(maxExpandSize.x * denominator, maxExpandSize.y)
171             : Vector2(maxExpandSize.x, maxExpandSize.y * denominator);
172 
173         // Add the expand space
174         minSize = addSize(expandSize, minSize);
175 
176     }
177 
178     protected override void drawImpl(Rectangle, Rectangle area) {
179 
180         assertClean(children, "Children were changed without calling updateSize().");
181 
182         auto position = start(area);
183 
184         foreach (child; filterChildren) {
185 
186             // Ignore if this child is not visible
187             if (child.isHidden) continue;
188 
189             // Get params
190             const size = childSpace(child, size(area), true);
191             const rect = Rectangle(
192                 position.x, position.y,
193                 size.x, size.y
194             );
195 
196             // Draw the child
197             drawChild(child, rect);
198 
199             // Offset position
200             position = childOffset(position, size);
201 
202         }
203 
204     }
205 
206     /// List children in the space, removing all nodes queued for deletion beforehand.
207     protected auto filterChildren() {
208 
209         struct ChildIterator {
210 
211             Space node;
212 
213             int opApply(int delegate(Node) @safe fun) @trusted {
214 
215                 foreach (_, node; this) {
216 
217                     if (auto result = fun(node)) {
218                         return result;
219                     }
220 
221                 }
222                 return 0;
223 
224             }
225 
226             int opApply(int delegate(size_t index, Node) @safe fun) @trusted {
227 
228                 node.children.lock();
229                 scope (exit) node.children.unlock();
230 
231                 size_t destinationIndex = 0;
232                 int end = 0;
233 
234                 // Iterate through all children. When we come upon ones that are queued for deletion,
235                 foreach (sourceIndex, child; node.children) {
236 
237                     const toRemove = child.toRemove;
238                     child.toRemove = false;
239 
240                     // Ignore children that are to be removed
241                     if (toRemove) continue;
242 
243                     // Yield the child
244                     if (!end)
245                         end = fun(destinationIndex, child);
246 
247                     // Move the child if needed
248                     if (sourceIndex != destinationIndex) {
249 
250                         node.children.forceMutable[destinationIndex] = child;
251 
252                     }
253 
254                     // Stop iteration if requested — and if there's nothing to move
255                     else if (end) return end;
256 
257                     // Set space for next nodes
258                     destinationIndex++;
259 
260 
261                 }
262 
263                 // Adjust length
264                 node.children.forceMutable.length = destinationIndex;
265 
266                 return 0;
267 
268             }
269 
270         }
271 
272         return ChildIterator(this);
273 
274     }
275 
276     /// Space does not take hover; isHovered is always false.
277     protected override bool hoveredImpl(Rectangle, Vector2) {
278 
279         return false;
280 
281     }
282 
283     /// Params:
284     ///     child     = Child size to add.
285     ///     previous  = Previous position.
286     private Vector2 addSize(Vector2 child, Vector2 previous) const {
287 
288         import std.algorithm : max;
289 
290         // Horizontal
291         if (directionHorizontal) {
292 
293             return Vector2(
294                 previous.x + child.x,
295                 max(minSize.y, child.y),
296             );
297 
298         }
299 
300         // Vertical
301         else return Vector2(
302             max(minSize.x, child.x),
303             previous.y + child.y,
304         );
305 
306     }
307 
308     /// Calculate the offset for the next node, given the `childSpace` result for its previous sibling.
309     protected Vector2 childOffset(Vector2 currentOffset, Vector2 childSpace) {
310 
311         if (isHorizontal)
312             return currentOffset + Vector2(childSpace.x + style.gap.sideX, 0);
313         else
314             return currentOffset + Vector2(0, childSpace.y + style.gap.sideY);
315 
316     }
317 
318     /// Get space for a child.
319     /// Params:
320     ///     child     = Child to place
321     ///     available = Available space
322     protected Vector2 childSpace(const Node child, Vector2 available, bool stateful = true) const
323     in(
324         child.isHidden || child.layout.expand <= denominator,
325         format!"Nodes %s/%s sizes are out of date, call updateSize after updating the tree or layout (%s/%s)"(
326             typeid(this), typeid(child), child.layout.expand, denominator,
327         )
328     )
329     out(
330         r; only(r.tupleof).all!isFinite,
331         format!"space: child %s given invalid size %s. available = %s, expand = %s, denominator = %s, reserved = %s"(
332             typeid(child), r, available, child.layout.expand, denominator, reservedSpace
333         )
334     )
335     do {
336 
337         // Hidden, give it no space
338         if (child.isHidden) return Vector2();
339 
340         // Horizontal
341         if (directionHorizontal) {
342 
343             const avail = (available.x - reservedSpace);
344             const minSize = stateful
345                 ? child.minSize.x
346                 : available.x;
347 
348             return Vector2(
349                 child.layout.expand
350                     ? avail * child.layout.expand / denominator
351                     : minSize,
352                 available.y,
353             );
354 
355         }
356 
357         // Vertical
358         else {
359 
360             const avail = (available.y - reservedSpace);
361             const minSize = stateful
362                 ? child.minSize.y
363                 : available.y;
364 
365             return Vector2(
366                 available.x,
367                 child.layout.expand
368                     ? avail * child.layout.expand / denominator
369                     : minSize,
370             );
371 
372         }
373 
374     }
375 
376 }
377 
378 ///
379 unittest {
380 
381     import fluid;
382 
383     // A vspace will align all its content in a column
384     vspace(
385         label("First entry"),
386         label("Second entry"),
387         label("Third entry"),
388     );
389 
390     // hspace will lay out the nodes in a row
391     hspace(
392         label("One, "),
393         label("Two, "),
394         label("Three!"),
395     );
396 
397     // Combine them to quickly build layouts!
398     vspace(
399         label("Are you sure you want to proceed?"),
400         hspace(
401             button("Yes", delegate { }),
402             button("Cancel", delegate { }),
403         ),
404     );
405 
406 }