1 module glui.map_space;
2 
3 import raylib;
4 
5 import std.conv;
6 import std.algorithm;
7 
8 import glui.node;
9 import glui.space;
10 import glui.style;
11 import glui.utils;
12 
13 
14 @safe:
15 
16 
17 alias mapSpace = simpleConstructor!GluiMapSpace;
18 
19 /// Defines the direction the node is "dropped from", that is, which corner of the object will be the anchor.
20 /// Defaults to `start, start`, therefore, the supplied coordinate refers to the top-left of the object.
21 ///
22 /// Automatic may be set to make it present common dropdown behavior — top-left by default, but will change if there
23 /// is overflow.
24 enum MapDropDirection {
25 
26     start, center, end, automatic
27 
28 }
29 
30 struct MapDropVector {
31 
32     MapDropDirection x, y;
33 
34 }
35 
36 struct MapPosition {
37 
38     Vector2 coords;
39     MapDropVector drop;
40 
41     alias coords this;
42 
43 }
44 
45 MapDropVector dropVector()() {
46 
47     return MapDropVector.init;
48 
49 }
50 
51 MapDropVector dropVector(string dropXY)() {
52 
53     return dropVector!(dropXY, dropXY);
54 
55 }
56 
57 MapDropVector dropVector(string dropX, string dropY)() {
58 
59     enum val(string dropV) = dropV == "auto"
60         ? MapDropDirection.automatic
61         : dropV.to!MapDropDirection;
62 
63     return MapDropVector(val!dropX, val!dropY);
64 
65 }
66 
67 class GluiMapSpace : GluiSpace {
68 
69     mixin DefineStyles;
70 
71     alias DropDirection = MapDropDirection;
72     alias DropVector = MapDropVector;
73     alias Position = MapPosition;
74 
75     /// Mapping of nodes to their positions.
76     Position[GluiNode] positions;
77 
78     /// If true, the node will prevent its children from leaving the screen space.
79     bool preventOverlap;
80 
81     static foreach (index; 0..BasicNodeParamLength) {
82 
83         /// Construct the space. Arguments are either nodes, or positions/vectors affecting the next node added through
84         /// the constructor.
85         this(T...)(BasicNodeParam!index params, T children)
86         if (!T.length || is(T[0] == Vector2) || is(T[0] == DropVector) || is(T[0] == Position) || is(T[0] : GluiNode)) {
87 
88             super(params);
89 
90             Position position;
91 
92             static foreach (child; children) {
93 
94                 // Update position
95                 static if (is(typeof(child) == Position)) {
96 
97                     position = child;
98 
99                 }
100 
101                 else static if (is(typeof(child) == MapDropVector)) {
102 
103                     position.drop = child;
104 
105                 }
106 
107                 else static if (is(typeof(child) == Vector2)) {
108 
109                     position.coords = child;
110 
111                 }
112 
113                 // Add child
114                 else {
115 
116                     addChild(child, position);
117                     position = Position.init;
118 
119                 }
120 
121             }
122 
123         }
124 
125     }
126 
127     /// Add a new child to the space and assign it some position.
128     void addChild(GluiNode node, Position position) {
129 
130         children ~= node;
131         positions[node] = position;
132         updateSize();
133 
134     }
135 
136     void moveChild(GluiNode node, Position position) {
137 
138         positions[node] = position;
139 
140     }
141 
142     void moveChild(GluiNode node, Vector2 vector) {
143 
144         positions[node].coords = vector;
145 
146     }
147 
148     void moveChild(GluiNode node, DropVector vector) {
149 
150         positions[node].drop = vector;
151 
152     }
153 
154     protected override void resizeImpl(Vector2 space) {
155 
156         minSize = Vector2(0, 0);
157 
158         // TODO get rid of position entries for removed elements
159 
160         foreach (child; children) {
161 
162             const position = positions[child];
163 
164             child.resize(tree, theme, space);
165 
166             // Get the child's end corner
167             const endCorner = getEndCorner(space, child, position);
168 
169             minSize.x = max(minSize.x, endCorner.x);
170             minSize.y = max(minSize.y, endCorner.y);
171 
172         }
173 
174     }
175 
176     protected override void drawImpl(Rectangle outer, Rectangle inner) {
177 
178         drawChildren((child) {
179 
180             const position = positions.require(child, Position.init);
181             const space = Vector2(inner.w, inner.h);
182             const startCorner = getStartCorner(space, child, position);
183 
184             auto x = inner.x + startCorner.x;
185             auto y = inner.y + startCorner.y;
186 
187             if (preventOverlap) {
188 
189                 x = x.clamp(inner.x, inner.x + max(0, inner.w - child.minSize.x));
190                 y = y.clamp(inner.y, inner.y + max(0, inner.h - child.minSize.y));
191 
192             }
193 
194             const childRect = Rectangle(
195                 x, y,
196                 child.minSize.x, child.minSize.y
197             );
198 
199             // Draw the child
200             child.draw(childRect);
201 
202         });
203 
204     }
205 
206     private alias getStartCorner = getCorner!false;
207     private alias getEndCorner   = getCorner!true;
208 
209     private Vector2 getCorner(bool end)(Vector2 space, GluiNode child, Position position) {
210 
211         Vector2 result;
212 
213         // Get the children's corners
214         static foreach (direction; ['x', 'y']) {{
215 
216             const pos = mixin("position.coords." ~ direction);
217             const dropDirection = mixin("position.drop." ~ direction);
218             const childSize = mixin("child.minSize." ~ direction);
219 
220             const overflow = pos + childSize > mixin("space." ~ direction);
221 
222             static if (end)
223             mixin("result." ~ direction) = dropDirection.predSwitch(
224                 DropDirection.start,     pos + childSize,
225                 DropDirection.center,    pos + childSize/2,
226                 DropDirection.end,       pos,
227                 DropDirection.automatic, overflow ? pos : pos + childSize,
228             );
229 
230             else
231             mixin("result." ~ direction) = dropDirection.predSwitch(
232                 DropDirection.start,     pos,
233                 DropDirection.center,    pos - childSize/2,
234                 DropDirection.end,       pos - childSize,
235                 DropDirection.automatic, overflow ? pos - childSize : pos,
236             );
237 
238         }}
239 
240         return result;
241 
242     }
243 
244 }