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 deprecated("`MapFrame.mouseDrag` is legacy and will not continue to work with Fluid's new I/O system. " 197 ~ "You can use `dragChildBy` to move nodes, but you need to implement mouse controls yourself. " 198 ~ "Consequently, `mouseDrag` will be removed in Fluid 0.8.0.") { 199 200 Node mouseDrag(Node node) @trusted { 201 202 assert(node in positions, "Requested node is not present in the map"); 203 204 _mouseDrag = node; 205 _mousePosition = Vector2(float.nan, float.nan); 206 207 return node; 208 209 } 210 211 /// Get the node currently affected by mouseDrag. 212 inout(Node) mouseDrag() inout { return _mouseDrag; } 213 214 /// Stop current mouse movements 215 final void stopMouseDrag() { 216 217 _mouseDrag = null; 218 219 } 220 221 } 222 223 /// Drag the given child, changing its position relatively. 224 void dragChildBy(Node node, Vector2 delta) { 225 226 auto position = node in positions; 227 assert(position, "Dragged node is not present in the map"); 228 229 position.coords = Vector2(position.x + delta.x, position.y + delta.y); 230 231 } 232 233 protected override void resizeImpl(Vector2 space) { 234 235 minSize = Vector2(0, 0); 236 237 // TODO get rid of position entries for removed elements 238 239 foreach (child; children) { 240 241 const position = positions.require(child, MapPosition.init); 242 243 resizeChild(child, space); 244 245 // Get the child's end corner 246 const endCorner = getEndCorner(space, child, position); 247 248 minSize.x = max(minSize.x, endCorner.x); 249 minSize.y = max(minSize.y, endCorner.y); 250 251 } 252 253 } 254 255 protected override void drawImpl(Rectangle outer, Rectangle inner) { 256 257 /// Move the given box to mapFrame bounds 258 Vector2 moveToBounds(Vector2 coords, Vector2 size) { 259 260 // Ignore if no overflow prevention is enabled 261 if (!preventOverflow) return coords; 262 263 return Vector2( 264 coords.x.clamp(inner.x, inner.x + max(0, inner.width - size.x)), 265 coords.y.clamp(inner.y, inner.y + max(0, inner.height - size.y)), 266 ); 267 268 } 269 270 // Drag the current child 271 if (_mouseDrag) { 272 273 import std.math; 274 275 // Update the mouse position 276 auto mouse = tree.io.mousePosition; 277 scope (exit) _mousePosition = mouse; 278 279 // If the previous mouse position was NaN, we've just started dragging 280 if (isNaN(_mousePosition.x)) { 281 282 // Check their current position 283 auto position = _mouseDrag in positions; 284 assert(position, "Dragged node is not present in the map"); 285 286 // Keep them in bounds 287 position.coords = moveToBounds(position.coords, _mouseDrag.minSize); 288 289 } 290 291 else { 292 293 // Drag the child 294 dragChildBy(_mouseDrag, mouse - _mousePosition); 295 296 } 297 298 } 299 300 foreach (child; filterChildren) { 301 302 const position = positions.require(child, Position.init); 303 const space = Vector2(inner.w, inner.h); 304 const startCorner = getStartCorner(space, child, position); 305 306 auto vec = Vector2(inner.x, inner.y) + startCorner; 307 308 if (preventOverflow) { 309 310 vec = moveToBounds(vec, child.minSize); 311 312 } 313 314 const childRect = Rectangle( 315 vec.tupleof, 316 child.minSize.x, child.minSize.y 317 ); 318 319 // Draw the child 320 drawChild(child, childRect); 321 322 } 323 324 } 325 326 private alias getStartCorner = getCorner!false; 327 private alias getEndCorner = getCorner!true; 328 329 private Vector2 getCorner(bool end)(Vector2 space, Node child, Position position) { 330 331 Vector2 result; 332 333 // Get the children's corners 334 static foreach (direction; ['x', 'y']) {{ 335 336 const pos = mixin("position.coords." ~ direction); 337 const dropDirection = mixin("position.drop." ~ direction); 338 const childSize = mixin("child.minSize." ~ direction); 339 340 /// Get the value 341 float value(DropDirection targetDirection) { 342 343 /// Get the direction chosen by auto. 344 DropDirection autoDirection() { 345 346 // Check if it overflows on the end 347 const overflowEnd = pos + childSize > mixin("space." ~ direction); 348 349 // Drop from the start 350 if (!overflowEnd) return DropDirection.start; 351 352 // Check if it overflows on both sides 353 const overflowStart = pos - childSize < 0; 354 355 return overflowStart 356 ? DropDirection.center 357 : DropDirection.end; 358 359 } 360 361 static if (end) 362 return targetDirection.predSwitch( 363 DropDirection.start, pos + childSize, 364 DropDirection.center, pos + childSize/2, 365 DropDirection.end, pos, 366 DropDirection.automatic, value(autoDirection), 367 ); 368 369 else 370 return targetDirection.predSwitch( 371 DropDirection.start, pos, 372 DropDirection.center, pos - childSize/2, 373 DropDirection.end, pos - childSize, 374 DropDirection.automatic, value(autoDirection), 375 ); 376 377 } 378 379 mixin("result." ~ direction) = value(dropDirection); 380 381 }} 382 383 return result; 384 385 } 386 387 override void dropHover(Vector2 position, Rectangle rectangle) { 388 389 } 390 391 override void drop(Vector2, Rectangle rectangle, Node node) { 392 393 const position = MapPosition(rectangle.start); 394 395 // Already a child 396 if (children.canFind!"a is b"(node)) { 397 398 positions[node] = position; 399 400 } 401 402 // New child 403 else this.addChild(node, position); 404 405 } 406 407 }