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