1 module glui.map_space;
2 
3 import raylib;
4 
5 import std.conv;
6 import std.math;
7 import std.format;
8 import std.algorithm;
9 
10 import glui.node;
11 import glui.space;
12 import glui.style;
13 import glui.utils;
14 
15 
16 @safe:
17 
18 
19 alias mapSpace = simpleConstructor!GluiMapSpace;
20 
21 /// Defines the direction the node is "dropped from", that is, which corner of the object will be the anchor.
22 /// Defaults to `start, start`, therefore, the supplied coordinate refers to the top-left of the object.
23 ///
24 /// Automatic may be set to make it present common dropdown behavior — top-left by default, but will change if there
25 /// is overflow.
26 enum MapDropDirection {
27 
28     start, center, end, automatic
29 
30 }
31 
32 struct MapDropVector {
33 
34     MapDropDirection x, y;
35 
36 }
37 
38 struct MapPosition {
39 
40     Vector2 coords;
41     MapDropVector drop;
42 
43     alias coords this;
44 
45 }
46 
47 MapDropVector dropVector()() {
48 
49     return MapDropVector.init;
50 
51 }
52 
53 MapDropVector dropVector(string dropXY)() {
54 
55     return dropVector!(dropXY, dropXY);
56 
57 }
58 
59 MapDropVector dropVector(string dropX, string dropY)() {
60 
61     enum val(string dropV) = dropV == "auto"
62         ? MapDropDirection.automatic
63         : dropV.to!MapDropDirection;
64 
65     return MapDropVector(val!dropX, val!dropY);
66 
67 }
68 
69 class GluiMapSpace : GluiSpace {
70 
71     mixin DefineStyles;
72 
73     alias DropDirection = MapDropDirection;
74     alias DropVector = MapDropVector;
75     alias Position = MapPosition;
76 
77     /// Mapping of nodes to their positions.
78     Position[GluiNode] positions;
79 
80     /// If true, the node will prevent its children from leaving the screen space.
81     bool preventOverflow;
82 
83     deprecated("preventOverlap has been renamed to preventOverflow and will be removed in Glui 0.6.0")
84     ref inout(bool) preventOverlap() inout { return preventOverflow; }
85 
86     private {
87 
88         /// Last mouse position
89         Vector2 _mousePosition;
90 
91         /// Child currently dragged with the mouse.
92         ///
93         /// The child will move along with mouse movements performed by the user.
94         GluiNode _mouseDrag;
95 
96     }
97 
98     static foreach (index; 0..BasicNodeParamLength) {
99 
100         /// Construct the space. Arguments are either nodes, or positions/vectors affecting the next node added through
101         /// the constructor.
102         this(T...)(BasicNodeParam!index params, T children)
103         if (!T.length || is(T[0] == Vector2) || is(T[0] == DropVector) || is(T[0] == Position) || is(T[0] : GluiNode)) {
104 
105             super(params);
106 
107             Position position;
108 
109             static foreach (child; children) {
110 
111                 // Update position
112                 static if (is(typeof(child) == Position)) {
113 
114                     position = child;
115 
116                 }
117 
118                 else static if (is(typeof(child) == MapDropVector)) {
119 
120                     position.drop = child;
121 
122                 }
123 
124                 else static if (is(typeof(child) == Vector2)) {
125 
126                     position.coords = child;
127 
128                 }
129 
130                 // Add child
131                 else {
132 
133                     addChild(child, position);
134                     position = Position.init;
135 
136                 }
137 
138             }
139 
140         }
141 
142     }
143 
144     /// Add a new child to the space and assign it some position.
145     void addChild(GluiNode node, Position position)
146     in ([position.coords.tupleof].any!isFinite, format!"Given %s isn't valid, values must be finite"(position))
147     do {
148 
149         children ~= node;
150         positions[node] = position;
151         updateSize();
152     }
153 
154     void moveChild(GluiNode node, Position position)
155     in ([position.coords.tupleof].any!isFinite, format!"Given %s isn't valid, values must be finite"(position))
156     do {
157 
158         positions[node] = position;
159 
160     }
161 
162     void moveChild(GluiNode node, Vector2 vector)
163     in ([vector.tupleof].any!isFinite, format!"Given %s isn't valid, values must be finite"(vector))
164     do {
165 
166         positions[node].coords = vector;
167 
168     }
169 
170     void moveChild(GluiNode node, DropVector vector) {
171 
172         positions[node].drop = vector;
173 
174     }
175 
176     /// Make a node move relatively according to mouse position changes, making it behave as if it was being dragged by
177     /// the mouse.
178     GluiNode mouseDrag(GluiNode node) @trusted {
179 
180         assert(node in positions, "Requested node is not present in the map");
181 
182         _mouseDrag = node;
183         _mousePosition = Vector2(float.nan, float.nan);
184 
185         return node;
186 
187     }
188 
189     /// Get the node currently affected by mouseDrag.
190     inout(GluiNode) mouseDrag() inout { return _mouseDrag; }
191 
192     /// Stop current mouse movements
193     final void stopMouseDrag() {
194 
195         _mouseDrag = null;
196 
197     }
198 
199     /// Drag the given child, changing its position relatively.
200     void dragChildBy(GluiNode node, Vector2 delta) {
201 
202         auto position = node in positions;
203         assert(position, "Dragged node is not present in the map");
204 
205         position.coords = Vector2(position.x + delta.x, position.y + delta.y);
206 
207     }
208 
209     protected override void resizeImpl(Vector2 space) {
210 
211         minSize = Vector2(0, 0);
212 
213         // TODO get rid of position entries for removed elements
214 
215         foreach (child; children) {
216 
217             const position = positions[child];
218 
219             child.resize(tree, theme, space);
220 
221             // Get the child's end corner
222             const endCorner = getEndCorner(space, child, position);
223 
224             minSize.x = max(minSize.x, endCorner.x);
225             minSize.y = max(minSize.y, endCorner.y);
226 
227         }
228 
229     }
230 
231     protected override void drawImpl(Rectangle outer, Rectangle inner) {
232 
233         /// Move the given box to mapSpace bounds
234         Vector2 moveToBounds(Vector2 coords, Vector2 size) {
235 
236             // Ignore if no overflow prevention is enabled
237             if (!preventOverflow) return coords;
238 
239             return Vector2(
240                 coords.x.clamp(inner.x, inner.x + max(0, inner.width - size.x)),
241                 coords.y.clamp(inner.y, inner.y + max(0, inner.height - size.y)),
242             );
243 
244         }
245 
246         // Drag the current child
247         if (_mouseDrag) () @trusted {
248 
249             import std.math;
250 
251             // Update the mouse position
252             auto mouse = GetMousePosition();
253             scope (exit) _mousePosition = mouse;
254 
255             // If the previous mouse position was NaN, we've just started dragging
256             if (isNaN(_mousePosition.x)) {
257 
258                 // Check their current position
259                 auto position = _mouseDrag in positions;
260                 assert(position, "Dragged node is not present in the map");
261 
262                 // Keep them in bounds
263                 position.coords = moveToBounds(position.coords, _mouseDrag.minSize);
264 
265             }
266 
267             else {
268 
269                 // Drag the child
270                 dragChildBy(_mouseDrag, mouse - _mousePosition);
271 
272             }
273 
274         }();
275 
276         drawChildren((child) {
277 
278             const position = positions.require(child, Position.init);
279             const space = Vector2(inner.w, inner.h);
280             const startCorner = getStartCorner(space, child, position);
281 
282             auto vec = Vector2(inner.x, inner.y) + startCorner;
283 
284             if (preventOverflow) {
285 
286                 vec = moveToBounds(vec, child.minSize);
287 
288             }
289 
290             const childRect = Rectangle(
291                 vec.tupleof,
292                 child.minSize.x, child.minSize.y
293             );
294 
295             // Draw the child
296             child.draw(childRect);
297 
298         });
299 
300     }
301 
302     private alias getStartCorner = getCorner!false;
303     private alias getEndCorner   = getCorner!true;
304 
305     private Vector2 getCorner(bool end)(Vector2 space, GluiNode child, Position position) {
306 
307         Vector2 result;
308 
309         // Get the children's corners
310         static foreach (direction; ['x', 'y']) {{
311 
312             const pos = mixin("position.coords." ~ direction);
313             const dropDirection = mixin("position.drop." ~ direction);
314             const childSize = mixin("child.minSize." ~ direction);
315 
316             /// Get the value
317             float value(DropDirection targetDirection) {
318 
319                 /// Get the direction chosen by auto.
320                 DropDirection autoDirection() {
321 
322                     // Check if it overflows on the end
323                     const overflowEnd = pos + childSize > mixin("space." ~ direction);
324 
325                     // Drop from the start
326                     if (!overflowEnd) return DropDirection.start;
327 
328                     // Check if it overflows on both sides
329                     const overflowStart = pos - childSize < 0;
330 
331                     return overflowStart
332                         ? DropDirection.center
333                         : DropDirection.end;
334 
335                 }
336 
337                 static if (end)
338                 return targetDirection.predSwitch(
339                     DropDirection.start,     pos + childSize,
340                     DropDirection.center,    pos + childSize/2,
341                     DropDirection.end,       pos,
342                     DropDirection.automatic, value(autoDirection),
343                 );
344 
345                 else
346                 return targetDirection.predSwitch(
347                     DropDirection.start,     pos,
348                     DropDirection.center,    pos - childSize/2,
349                     DropDirection.end,       pos - childSize,
350                     DropDirection.automatic, value(autoDirection),
351                 );
352 
353             }
354 
355             mixin("result." ~ direction) = value(dropDirection);
356 
357         }}
358 
359         return result;
360 
361     }
362 
363 }