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     deprecated("`MapFrame.mouseDrag` is legacy and will not continue to work with Fluid's new I/O system. "
197         ~ "You can use `dragChildBy` to move nodes, but you need to implement mouse controls yourself. "
198         ~ "Consequently, `mouseDrag` will be removed in Fluid 0.8.0.") {
199 
200         Node mouseDrag(Node node) @trusted {
201 
202             assert(node in positions, "Requested node is not present in the map");
203 
204             _mouseDrag = node;
205             _mousePosition = Vector2(float.nan, float.nan);
206 
207             return node;
208 
209         }
210 
211         /// Get the node currently affected by mouseDrag.
212         inout(Node) mouseDrag() inout { return _mouseDrag; }
213 
214         /// Stop current mouse movements
215         final void stopMouseDrag() {
216 
217             _mouseDrag = null;
218 
219         }
220 
221     }
222 
223     /// Drag the given child, changing its position relatively.
224     void dragChildBy(Node node, Vector2 delta) {
225 
226         auto position = node in positions;
227         assert(position, "Dragged node is not present in the map");
228 
229         position.coords = Vector2(position.x + delta.x, position.y + delta.y);
230 
231     }
232 
233     protected override void resizeImpl(Vector2 space) {
234 
235         minSize = Vector2(0, 0);
236 
237         // TODO get rid of position entries for removed elements
238 
239         foreach (child; children) {
240 
241             const position = positions.require(child, MapPosition.init);
242 
243             resizeChild(child, space);
244 
245             // Get the child's end corner
246             const endCorner = getEndCorner(space, child, position);
247 
248             minSize.x = max(minSize.x, endCorner.x);
249             minSize.y = max(minSize.y, endCorner.y);
250 
251         }
252 
253     }
254 
255     protected override void drawImpl(Rectangle outer, Rectangle inner) {
256 
257         /// Move the given box to mapFrame bounds
258         Vector2 moveToBounds(Vector2 coords, Vector2 size) {
259 
260             // Ignore if no overflow prevention is enabled
261             if (!preventOverflow) return coords;
262 
263             return Vector2(
264                 coords.x.clamp(inner.x, inner.x + max(0, inner.width - size.x)),
265                 coords.y.clamp(inner.y, inner.y + max(0, inner.height - size.y)),
266             );
267 
268         }
269 
270         // Drag the current child
271         if (_mouseDrag) {
272 
273             import std.math;
274 
275             // Update the mouse position
276             auto mouse = tree.io.mousePosition;
277             scope (exit) _mousePosition = mouse;
278 
279             // If the previous mouse position was NaN, we've just started dragging
280             if (isNaN(_mousePosition.x)) {
281 
282                 // Check their current position
283                 auto position = _mouseDrag in positions;
284                 assert(position, "Dragged node is not present in the map");
285 
286                 // Keep them in bounds
287                 position.coords = moveToBounds(position.coords, _mouseDrag.minSize);
288 
289             }
290 
291             else {
292 
293                 // Drag the child
294                 dragChildBy(_mouseDrag, mouse - _mousePosition);
295 
296             }
297 
298         }
299 
300         foreach (child; filterChildren) {
301 
302             const position = positions.require(child, Position.init);
303             const space = Vector2(inner.w, inner.h);
304             const startCorner = getStartCorner(space, child, position);
305 
306             auto vec = Vector2(inner.x, inner.y) + startCorner;
307 
308             if (preventOverflow) {
309 
310                 vec = moveToBounds(vec, child.minSize);
311 
312             }
313 
314             const childRect = Rectangle(
315                 vec.tupleof,
316                 child.minSize.x, child.minSize.y
317             );
318 
319             // Draw the child
320             drawChild(child, childRect);
321 
322         }
323 
324     }
325 
326     private alias getStartCorner = getCorner!false;
327     private alias getEndCorner   = getCorner!true;
328 
329     private Vector2 getCorner(bool end)(Vector2 space, Node child, Position position) {
330 
331         Vector2 result;
332 
333         // Get the children's corners
334         static foreach (direction; ['x', 'y']) {{
335 
336             const pos = mixin("position.coords." ~ direction);
337             const dropDirection = mixin("position.drop." ~ direction);
338             const childSize = mixin("child.minSize." ~ direction);
339 
340             /// Get the value
341             float value(DropDirection targetDirection) {
342 
343                 /// Get the direction chosen by auto.
344                 DropDirection autoDirection() {
345 
346                     // Check if it overflows on the end
347                     const overflowEnd = pos + childSize > mixin("space." ~ direction);
348 
349                     // Drop from the start
350                     if (!overflowEnd) return DropDirection.start;
351 
352                     // Check if it overflows on both sides
353                     const overflowStart = pos - childSize < 0;
354 
355                     return overflowStart
356                         ? DropDirection.center
357                         : DropDirection.end;
358 
359                 }
360 
361                 static if (end)
362                 return targetDirection.predSwitch(
363                     DropDirection.start,     pos + childSize,
364                     DropDirection.center,    pos + childSize/2,
365                     DropDirection.end,       pos,
366                     DropDirection.automatic, value(autoDirection),
367                 );
368 
369                 else
370                 return targetDirection.predSwitch(
371                     DropDirection.start,     pos,
372                     DropDirection.center,    pos - childSize/2,
373                     DropDirection.end,       pos - childSize,
374                     DropDirection.automatic, value(autoDirection),
375                 );
376 
377             }
378 
379             mixin("result." ~ direction) = value(dropDirection);
380 
381         }}
382 
383         return result;
384 
385     }
386 
387     override void dropHover(Vector2 position, Rectangle rectangle) {
388 
389     }
390 
391     override void drop(Vector2, Rectangle rectangle, Node node) {
392 
393         const position = MapPosition(rectangle.start);
394 
395         // Already a child
396         if (children.canFind!"a is b"(node)) {
397 
398             positions[node] = position;
399 
400         }
401 
402         // New child
403         else this.addChild(node, position);
404 
405     }
406 
407 }