1 module fluid.map_space;
2 
3 import std.conv;
4 import std.math;
5 import std.format;
6 import std.algorithm;
7 
8 import fluid.node;
9 import fluid.input;
10 import fluid.space;
11 import fluid.style;
12 import fluid.utils;
13 import fluid.actions;
14 import fluid.backend;
15 import fluid.container;
16 
17 
18 @safe:
19 
20 
21 alias mapSpace = simpleConstructor!MapSpace;
22 
23 /// Defines the direction the node is "dropped from", that is, which corner of the object will be the anchor.
24 /// Defaults to `start, start`, therefore, the supplied coordinate refers to the top-left of the object.
25 ///
26 /// Automatic may be set to make it present common dropdown behavior — top-left by default, but will change if there
27 /// is overflow.
28 enum MapDropDirection {
29 
30     start, center, end, automatic
31 
32 }
33 
34 struct MapDropVector {
35 
36     MapDropDirection x, y;
37 
38 }
39 
40 struct MapPosition {
41 
42     Vector2 coords;
43     MapDropVector drop;
44 
45     alias coords this;
46 
47 }
48 
49 MapDropVector dropVector()() {
50 
51     return MapDropVector.init;
52 
53 }
54 
55 MapDropVector dropVector(string dropXY)() {
56 
57     return dropVector!(dropXY, dropXY);
58 
59 }
60 
61 MapDropVector dropVector(string dropX, string dropY)() {
62 
63     enum val(string dropV) = dropV == "auto"
64         ? MapDropDirection.automatic
65         : dropV.to!MapDropDirection;
66 
67     return MapDropVector(val!dropX, val!dropY);
68 
69 }
70 
71 class MapSpace : Space {
72 
73     mixin DefineStyles;
74 
75     alias DropDirection = MapDropDirection;
76     alias DropVector = MapDropVector;
77     alias Position = MapPosition;
78 
79     /// Mapping of nodes to their positions.
80     Position[Node] positions;
81 
82     /// If true, the node will prevent its children from leaving the screen space.
83     bool preventOverflow;
84 
85     private {
86 
87         /// Last mouse position
88         Vector2 _mousePosition;
89 
90         /// Child currently dragged with the mouse.
91         ///
92         /// The child will move along with mouse movements performed by the user.
93         Node _mouseDrag;
94 
95     }
96 
97     /// Construct the space. Arguments are either nodes, or positions/vectors affecting the next node added through
98     /// the constructor.
99     this(T...)(NodeParams params, T children)
100     if (!T.length || is(T[0] == Vector2) || is(T[0] == DropVector) || is(T[0] == Position) || is(T[0] : Node)) {
101 
102         super(params);
103 
104         Position position;
105 
106         static foreach (child; children) {
107 
108             // Update position
109             static if (is(typeof(child) == Position)) {
110 
111                 position = child;
112 
113             }
114 
115             else static if (is(typeof(child) == MapDropVector)) {
116 
117                 position.drop = child;
118 
119             }
120 
121             else static if (is(typeof(child) == Vector2)) {
122 
123                 position.coords = child;
124 
125             }
126 
127             // Add child
128             else {
129 
130                 addChild(child, position);
131                 position = Position.init;
132 
133             }
134 
135         }
136 
137     }
138 
139     deprecated("Use this(NodeParams, T) instead") {
140 
141         static foreach (index; 0..BasicNodeParamLength) {
142 
143             /// Construct the space. Arguments are either nodes, or positions/vectors affecting the next node added through
144             /// the constructor.
145             this(T...)(BasicNodeParam!index params, T children)
146             if (!T.length || is(T[0] == Vector2) || is(T[0] == DropVector) || is(T[0] == Position) || is(T[0] : Node)) {
147 
148                 super(params);
149 
150                 Position position;
151 
152                 static foreach (child; children) {
153 
154                     // Update position
155                     static if (is(typeof(child) == Position)) {
156 
157                         position = child;
158 
159                     }
160 
161                     else static if (is(typeof(child) == MapDropVector)) {
162 
163                         position.drop = child;
164 
165                     }
166 
167                     else static if (is(typeof(child) == Vector2)) {
168 
169                         position.coords = child;
170 
171                     }
172 
173                     // Add child
174                     else {
175 
176                         addChild(child, position);
177                         position = Position.init;
178 
179                     }
180 
181                 }
182 
183             }
184 
185         }
186 
187     }
188 
189     /// Add a new child to the space and assign it some position.
190     void addChild(Node node, Position position)
191     in ([position.coords.tupleof].any!isFinite, format!"Given %s isn't valid, values must be finite"(position))
192     do {
193 
194         children ~= node;
195         positions[node] = position;
196         updateSize();
197     }
198 
199     /// ditto
200     void addFocusedChild(Node node, Position position) {
201 
202         addChild(node, position);
203         node.focusRecurse();
204 
205     }
206 
207     void moveChild(Node node, Position position)
208     in ([position.coords.tupleof].any!isFinite, format!"Given %s isn't valid, values must be finite"(position))
209     do {
210 
211         positions[node] = position;
212 
213     }
214 
215     void moveChild(Node node, Vector2 vector)
216     in ([vector.tupleof].any!isFinite, format!"Given %s isn't valid, values must be finite"(vector))
217     do {
218 
219         positions[node].coords = vector;
220 
221     }
222 
223     void moveChild(Node node, DropVector vector) {
224 
225         positions[node].drop = vector;
226 
227     }
228 
229     /// Make a node move relatively according to mouse position changes, making it behave as if it was being dragged by
230     /// the mouse.
231     Node mouseDrag(Node node) @trusted {
232 
233         assert(node in positions, "Requested node is not present in the map");
234 
235         _mouseDrag = node;
236         _mousePosition = Vector2(float.nan, float.nan);
237 
238         return node;
239 
240     }
241 
242     /// Get the node currently affected by mouseDrag.
243     inout(Node) mouseDrag() inout { return _mouseDrag; }
244 
245     /// Stop current mouse movements
246     final void stopMouseDrag() {
247 
248         _mouseDrag = null;
249 
250     }
251 
252     /// Drag the given child, changing its position relatively.
253     void dragChildBy(Node node, Vector2 delta) {
254 
255         auto position = node in positions;
256         assert(position, "Dragged node is not present in the map");
257 
258         position.coords = Vector2(position.x + delta.x, position.y + delta.y);
259 
260     }
261 
262     protected override void resizeImpl(Vector2 space) {
263 
264         minSize = Vector2(0, 0);
265 
266         // TODO get rid of position entries for removed elements
267 
268         foreach (child; children) {
269 
270             const position = positions[child];
271 
272             child.resize(tree, theme, space);
273 
274             // Get the child's end corner
275             const endCorner = getEndCorner(space, child, position);
276 
277             minSize.x = max(minSize.x, endCorner.x);
278             minSize.y = max(minSize.y, endCorner.y);
279 
280         }
281 
282     }
283 
284     protected override void drawImpl(Rectangle outer, Rectangle inner) {
285 
286         /// Move the given box to mapSpace bounds
287         Vector2 moveToBounds(Vector2 coords, Vector2 size) {
288 
289             // Ignore if no overflow prevention is enabled
290             if (!preventOverflow) return coords;
291 
292             return Vector2(
293                 coords.x.clamp(inner.x, inner.x + max(0, inner.width - size.x)),
294                 coords.y.clamp(inner.y, inner.y + max(0, inner.height - size.y)),
295             );
296 
297         }
298 
299         // Drag the current child
300         if (_mouseDrag) {
301 
302             import std.math;
303 
304             // Update the mouse position
305             auto mouse = tree.io.mousePosition;
306             scope (exit) _mousePosition = mouse;
307 
308             // If the previous mouse position was NaN, we've just started dragging
309             if (isNaN(_mousePosition.x)) {
310 
311                 // Check their current position
312                 auto position = _mouseDrag in positions;
313                 assert(position, "Dragged node is not present in the map");
314 
315                 // Keep them in bounds
316                 position.coords = moveToBounds(position.coords, _mouseDrag.minSize);
317 
318             }
319 
320             else {
321 
322                 // Drag the child
323                 dragChildBy(_mouseDrag, mouse - _mousePosition);
324 
325             }
326 
327         }
328 
329         foreach (child; filterChildren) {
330 
331             const position = positions.require(child, Position.init);
332             const space = Vector2(inner.w, inner.h);
333             const startCorner = getStartCorner(space, child, position);
334 
335             auto vec = Vector2(inner.x, inner.y) + startCorner;
336 
337             if (preventOverflow) {
338 
339                 vec = moveToBounds(vec, child.minSize);
340 
341             }
342 
343             const childRect = Rectangle(
344                 vec.tupleof,
345                 child.minSize.x, child.minSize.y
346             );
347 
348             // Draw the child
349             child.draw(childRect);
350 
351         }
352 
353     }
354 
355     private alias getStartCorner = getCorner!false;
356     private alias getEndCorner   = getCorner!true;
357 
358     private Vector2 getCorner(bool end)(Vector2 space, Node child, Position position) {
359 
360         Vector2 result;
361 
362         // Get the children's corners
363         static foreach (direction; ['x', 'y']) {{
364 
365             const pos = mixin("position.coords." ~ direction);
366             const dropDirection = mixin("position.drop." ~ direction);
367             const childSize = mixin("child.minSize." ~ direction);
368 
369             /// Get the value
370             float value(DropDirection targetDirection) {
371 
372                 /// Get the direction chosen by auto.
373                 DropDirection autoDirection() {
374 
375                     // Check if it overflows on the end
376                     const overflowEnd = pos + childSize > mixin("space." ~ direction);
377 
378                     // Drop from the start
379                     if (!overflowEnd) return DropDirection.start;
380 
381                     // Check if it overflows on both sides
382                     const overflowStart = pos - childSize < 0;
383 
384                     return overflowStart
385                         ? DropDirection.center
386                         : DropDirection.end;
387 
388                 }
389 
390                 static if (end)
391                 return targetDirection.predSwitch(
392                     DropDirection.start,     pos + childSize,
393                     DropDirection.center,    pos + childSize/2,
394                     DropDirection.end,       pos,
395                     DropDirection.automatic, value(autoDirection),
396                 );
397 
398                 else
399                 return targetDirection.predSwitch(
400                     DropDirection.start,     pos,
401                     DropDirection.center,    pos - childSize/2,
402                     DropDirection.end,       pos - childSize,
403                     DropDirection.automatic, value(autoDirection),
404                 );
405 
406             }
407 
408             mixin("result." ~ direction) = value(dropDirection);
409 
410         }}
411 
412         return result;
413 
414     }
415 
416     unittest {
417 
418         import fluid.structs : layout;
419 
420         class RectangleSpace : Space {
421 
422             Color color;
423 
424             this(Color color) @safe {
425                 super(NodeParams.init);
426                 this.color = color;
427             }
428 
429             override void resizeImpl(Vector2) @safe {
430                 minSize = Vector2(10, 10);
431             }
432 
433             override void drawImpl(Rectangle outer, Rectangle inner) @safe {
434                 io.drawRectangle(inner, color);
435             }
436 
437         }
438 
439         auto io = new HeadlessBackend;
440         auto root = mapSpace(
441             layout!"fill",
442 
443             // Rectangles with same X and Y
444 
445             Vector2(50, 50),
446             .dropVector!"start",
447             new RectangleSpace(color!"f00"),
448 
449             Vector2(50, 50),
450             .dropVector!"center",
451             new RectangleSpace(color!"0f0"),
452 
453             Vector2(50, 50),
454             .dropVector!"end",
455             new RectangleSpace(color!"00f"),
456 
457             // Rectangles with different Xs
458 
459             Vector2(50, 100),
460             .dropVector!("start", "start"),
461             new RectangleSpace(color!"e00"),
462 
463             Vector2(50, 100),
464             .dropVector!("center", "start"),
465             new RectangleSpace(color!"0e0"),
466 
467             Vector2(50, 100),
468             .dropVector!("end", "start"),
469             new RectangleSpace(color!"00e"),
470 
471             // Overflowing rectangles
472             Vector2(-10, -10),
473             new RectangleSpace(color!"f0f"),
474 
475             Vector2(20, -5),
476             new RectangleSpace(color!"0ff"),
477 
478             Vector2(-5, 20),
479             new RectangleSpace(color!"ff0"),
480         );
481 
482         root.io = io;
483         root.theme = nullTheme;
484 
485         foreach (preventOverflow; [false, true, false]) {
486 
487             root.preventOverflow = preventOverflow;
488             root.draw();
489 
490             // Every rectangle is attached to (50, 50) but using a different origin point
491             // The first red rectangle is attached by its start corner, the green by center corner, and the blue by end
492             // corner
493             io.assertRectangle(Rectangle(50, 50, 10, 10), color!"f00");
494             io.assertRectangle(Rectangle(45, 45, 10, 10), color!"0f0");
495             io.assertRectangle(Rectangle(40, 40, 10, 10), color!"00f");
496 
497             // This is similar for the second triple of rectangles, but the Y axis is the same for every one of them
498             io.assertRectangle(Rectangle(50, 100, 10, 10), color!"e00");
499             io.assertRectangle(Rectangle(45, 100, 10, 10), color!"0e0");
500             io.assertRectangle(Rectangle(40, 100, 10, 10), color!"00e");
501 
502             if (preventOverflow) {
503 
504                 // Two rectangles overflow: one is completely outside the view, and one is only peeking in
505                 // With overflow disabled, they should both be moved strictly inside the mapSpace
506                 io.assertRectangle(Rectangle(0, 0, 10, 10), color!"f0f");
507                 io.assertRectangle(Rectangle(20, 0, 10, 10), color!"0ff");
508                 io.assertRectangle(Rectangle(0, 20, 10, 10), color!"ff0");
509 
510             }
511 
512             else {
513 
514                 // With overflow enabled, these two overflows should now be allowed to stay outside
515                 io.assertRectangle(Rectangle(-10, -10, 10, 10), color!"f0f");
516                 io.assertRectangle(Rectangle(20, -5, 10, 10), color!"0ff");
517                 io.assertRectangle(Rectangle(-5, 20, 10, 10), color!"ff0");
518 
519             }
520 
521         }
522 
523     }
524 
525 }