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 }