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 Node mouseDrag(Node node) @trusted { 197 198 assert(node in positions, "Requested node is not present in the map"); 199 200 _mouseDrag = node; 201 _mousePosition = Vector2(float.nan, float.nan); 202 203 return node; 204 205 } 206 207 /// Get the node currently affected by mouseDrag. 208 inout(Node) mouseDrag() inout { return _mouseDrag; } 209 210 /// Stop current mouse movements 211 final void stopMouseDrag() { 212 213 _mouseDrag = null; 214 215 } 216 217 /// Drag the given child, changing its position relatively. 218 void dragChildBy(Node node, Vector2 delta) { 219 220 auto position = node in positions; 221 assert(position, "Dragged node is not present in the map"); 222 223 position.coords = Vector2(position.x + delta.x, position.y + delta.y); 224 225 } 226 227 protected override void resizeImpl(Vector2 space) { 228 229 minSize = Vector2(0, 0); 230 231 // TODO get rid of position entries for removed elements 232 233 foreach (child; children) { 234 235 const position = positions.require(child, MapPosition.init); 236 237 child.resize(tree, theme, space); 238 239 // Get the child's end corner 240 const endCorner = getEndCorner(space, child, position); 241 242 minSize.x = max(minSize.x, endCorner.x); 243 minSize.y = max(minSize.y, endCorner.y); 244 245 } 246 247 } 248 249 protected override void drawImpl(Rectangle outer, Rectangle inner) { 250 251 /// Move the given box to mapFrame bounds 252 Vector2 moveToBounds(Vector2 coords, Vector2 size) { 253 254 // Ignore if no overflow prevention is enabled 255 if (!preventOverflow) return coords; 256 257 return Vector2( 258 coords.x.clamp(inner.x, inner.x + max(0, inner.width - size.x)), 259 coords.y.clamp(inner.y, inner.y + max(0, inner.height - size.y)), 260 ); 261 262 } 263 264 // Drag the current child 265 if (_mouseDrag) { 266 267 import std.math; 268 269 // Update the mouse position 270 auto mouse = tree.io.mousePosition; 271 scope (exit) _mousePosition = mouse; 272 273 // If the previous mouse position was NaN, we've just started dragging 274 if (isNaN(_mousePosition.x)) { 275 276 // Check their current position 277 auto position = _mouseDrag in positions; 278 assert(position, "Dragged node is not present in the map"); 279 280 // Keep them in bounds 281 position.coords = moveToBounds(position.coords, _mouseDrag.minSize); 282 283 } 284 285 else { 286 287 // Drag the child 288 dragChildBy(_mouseDrag, mouse - _mousePosition); 289 290 } 291 292 } 293 294 foreach (child; filterChildren) { 295 296 const position = positions.require(child, Position.init); 297 const space = Vector2(inner.w, inner.h); 298 const startCorner = getStartCorner(space, child, position); 299 300 auto vec = Vector2(inner.x, inner.y) + startCorner; 301 302 if (preventOverflow) { 303 304 vec = moveToBounds(vec, child.minSize); 305 306 } 307 308 const childRect = Rectangle( 309 vec.tupleof, 310 child.minSize.x, child.minSize.y 311 ); 312 313 // Draw the child 314 child.draw(childRect); 315 316 } 317 318 } 319 320 private alias getStartCorner = getCorner!false; 321 private alias getEndCorner = getCorner!true; 322 323 private Vector2 getCorner(bool end)(Vector2 space, Node child, Position position) { 324 325 Vector2 result; 326 327 // Get the children's corners 328 static foreach (direction; ['x', 'y']) {{ 329 330 const pos = mixin("position.coords." ~ direction); 331 const dropDirection = mixin("position.drop." ~ direction); 332 const childSize = mixin("child.minSize." ~ direction); 333 334 /// Get the value 335 float value(DropDirection targetDirection) { 336 337 /// Get the direction chosen by auto. 338 DropDirection autoDirection() { 339 340 // Check if it overflows on the end 341 const overflowEnd = pos + childSize > mixin("space." ~ direction); 342 343 // Drop from the start 344 if (!overflowEnd) return DropDirection.start; 345 346 // Check if it overflows on both sides 347 const overflowStart = pos - childSize < 0; 348 349 return overflowStart 350 ? DropDirection.center 351 : DropDirection.end; 352 353 } 354 355 static if (end) 356 return targetDirection.predSwitch( 357 DropDirection.start, pos + childSize, 358 DropDirection.center, pos + childSize/2, 359 DropDirection.end, pos, 360 DropDirection.automatic, value(autoDirection), 361 ); 362 363 else 364 return targetDirection.predSwitch( 365 DropDirection.start, pos, 366 DropDirection.center, pos - childSize/2, 367 DropDirection.end, pos - childSize, 368 DropDirection.automatic, value(autoDirection), 369 ); 370 371 } 372 373 mixin("result." ~ direction) = value(dropDirection); 374 375 }} 376 377 return result; 378 379 } 380 381 unittest { 382 383 import fluid.space; 384 import fluid.structs : layout; 385 386 class RectangleSpace : Space { 387 388 Color color; 389 390 this(Color color) @safe { 391 this.color = color; 392 } 393 394 override void resizeImpl(Vector2) @safe { 395 minSize = Vector2(10, 10); 396 } 397 398 override void drawImpl(Rectangle outer, Rectangle inner) @safe { 399 io.drawRectangle(inner, color); 400 } 401 402 } 403 404 auto io = new HeadlessBackend; 405 auto root = mapFrame( 406 layout!"fill", 407 408 // Rectangles with same X and Y 409 410 Vector2(50, 50), 411 .dropVector!"start", 412 new RectangleSpace(color!"f00"), 413 414 Vector2(50, 50), 415 .dropVector!"center", 416 new RectangleSpace(color!"0f0"), 417 418 Vector2(50, 50), 419 .dropVector!"end", 420 new RectangleSpace(color!"00f"), 421 422 // Rectangles with different Xs 423 424 Vector2(50, 100), 425 .dropVector!("start", "start"), 426 new RectangleSpace(color!"e00"), 427 428 Vector2(50, 100), 429 .dropVector!("center", "start"), 430 new RectangleSpace(color!"0e0"), 431 432 Vector2(50, 100), 433 .dropVector!("end", "start"), 434 new RectangleSpace(color!"00e"), 435 436 // Overflowing rectangles 437 Vector2(-10, -10), 438 new RectangleSpace(color!"f0f"), 439 440 Vector2(20, -5), 441 new RectangleSpace(color!"0ff"), 442 443 Vector2(-5, 20), 444 new RectangleSpace(color!"ff0"), 445 ); 446 447 root.io = io; 448 root.theme = nullTheme; 449 450 foreach (preventOverflow; [false, true, false]) { 451 452 root.preventOverflow = preventOverflow; 453 root.draw(); 454 455 // Every rectangle is attached to (50, 50) but using a different origin point 456 // The first red rectangle is attached by its start corner, the green by center corner, and the blue by end 457 // corner 458 io.assertRectangle(Rectangle(50, 50, 10, 10), color!"f00"); 459 io.assertRectangle(Rectangle(45, 45, 10, 10), color!"0f0"); 460 io.assertRectangle(Rectangle(40, 40, 10, 10), color!"00f"); 461 462 // This is similar for the second triple of rectangles, but the Y axis is the same for every one of them 463 io.assertRectangle(Rectangle(50, 100, 10, 10), color!"e00"); 464 io.assertRectangle(Rectangle(45, 100, 10, 10), color!"0e0"); 465 io.assertRectangle(Rectangle(40, 100, 10, 10), color!"00e"); 466 467 if (preventOverflow) { 468 469 // Two rectangles overflow: one is completely outside the view, and one is only peeking in 470 // With overflow disabled, they should both be moved strictly inside the mapFrame 471 io.assertRectangle(Rectangle(0, 0, 10, 10), color!"f0f"); 472 io.assertRectangle(Rectangle(20, 0, 10, 10), color!"0ff"); 473 io.assertRectangle(Rectangle(0, 20, 10, 10), color!"ff0"); 474 475 } 476 477 else { 478 479 // With overflow enabled, these two overflows should now be allowed to stay outside 480 io.assertRectangle(Rectangle(-10, -10, 10, 10), color!"f0f"); 481 io.assertRectangle(Rectangle(20, -5, 10, 10), color!"0ff"); 482 io.assertRectangle(Rectangle(-5, 20, 10, 10), color!"ff0"); 483 484 } 485 486 } 487 488 } 489 490 override void dropHover(Vector2 position, Rectangle rectangle) { 491 492 } 493 494 override void drop(Vector2, Rectangle rectangle, Node node) { 495 496 const position = MapPosition(rectangle.start); 497 498 // Already a child 499 if (children.canFind!"a is b"(node)) { 500 501 positions[node] = position; 502 503 } 504 505 // New child 506 else this.addChild(node, position); 507 508 } 509 510 }