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