1 /// 2 module fluid.space; 3 4 import std.math; 5 import std.range; 6 import std.string; 7 import std.traits; 8 import std.algorithm; 9 10 import fluid.node; 11 import fluid.style; 12 import fluid.utils; 13 import fluid.backend; 14 import fluid.children; 15 import fluid.container; 16 17 18 @safe: 19 20 21 /// This is a space, a basic container for other nodes. 22 /// 23 /// Nodes are laid in a column (`vframe`) or in a row (`hframe`). 24 /// 25 /// Space only acts as a container and doesn't implement styles and doesn't take focus. It's very useful as a helper for 26 /// building layout, while `Frame` remains to provide styling. 27 alias vspace = simpleConstructor!Space; 28 29 /// ditto 30 alias hspace = simpleConstructor!(Space, (a) { 31 32 a.directionHorizontal = true; 33 34 }); 35 36 /// ditto 37 class Space : Node, FluidContainer { 38 39 mixin DefineStyles; 40 41 /// Children of this frame. 42 Children children; 43 44 /// Defines in what directions children of this frame should be placed. 45 /// 46 /// If true, children are placed horizontally, if false, vertically. 47 bool horizontal; 48 49 alias directionHorizontal = horizontal; 50 51 private { 52 53 /// Denominator for content sizing. 54 uint denominator; 55 56 /// Space reserved for shrinking elements. 57 uint reservedSpace; 58 59 } 60 61 // Generate constructors 62 deprecated("Use this(NodeParams params, Node[] nodes...) instead") { 63 64 static foreach (index; 0 .. BasicNodeParamLength) { 65 66 this(BasicNodeParam!index params, Node[] nodes...) { 67 68 super(params); 69 this.children ~= nodes; 70 71 } 72 73 } 74 75 } 76 77 this(NodeParams params, Node[] nodes...) { 78 79 super(params); 80 this.children ~= nodes; 81 82 } 83 84 this() { 85 86 } 87 88 /// Add children. 89 pragma(inline, true) 90 void opOpAssign(string operator : "~", T)(T nodes) { 91 92 children ~= nodes; 93 94 } 95 96 override Rectangle shallowScrollTo(const Node, Vector2, Rectangle, Rectangle childBox) { 97 98 // no-op, reordering should not be done without explicit orders 99 return childBox; 100 101 } 102 103 protected override void resizeImpl(Vector2 available) { 104 105 import std.algorithm : max, map, fold; 106 107 // Now that we're recalculating the layout, we can remove the dirty flag from children 108 children.clearDirty; 109 110 // Reset size 111 minSize = Vector2(0, 0); 112 reservedSpace = 0; 113 denominator = 0; 114 115 // Ignore the rest if there's no children 116 if (!children.length) return; 117 118 Vector2 maxExpandSize; 119 120 // Collect expanding children in a separate array 121 Node[] expandChildren; 122 foreach (child; children) { 123 124 // This node expands and isn't hidden 125 if (child.layout.expand && !child.isHidden) { 126 127 // Make it happen later 128 expandChildren ~= child; 129 130 // Add to the denominator 131 denominator += child.layout.expand; 132 133 } 134 135 // Check non-expand nodes now 136 else { 137 138 child.resize(tree, theme, childSpace(child, available, false)); 139 minSize = childPosition(child.minSize, minSize); 140 141 // Reserve space for this node 142 reservedSpace += directionHorizontal 143 ? cast(uint) child.minSize.x 144 : cast(uint) child.minSize.y; 145 146 } 147 148 } 149 150 // Calculate the size of expanding children last 151 foreach (child; expandChildren) { 152 153 // Resize the child 154 child.resize(tree, theme, childSpace(child, available, false)); 155 156 const childSize = child.minSize; 157 const childExpand = child.layout.expand; 158 159 const segmentSize = horizontal 160 ? Vector2(childSize.x / childExpand, childSize.y) 161 : Vector2(childSize.x, childSize.y / childExpand); 162 163 // Reserve expand space 164 maxExpandSize.x = max(maxExpandSize.x, segmentSize.x); 165 maxExpandSize.y = max(maxExpandSize.y, segmentSize.y); 166 167 } 168 169 const expandSize = horizontal 170 ? Vector2(maxExpandSize.x * denominator, maxExpandSize.y) 171 : Vector2(maxExpandSize.x, maxExpandSize.y * denominator); 172 173 // Add the expand space 174 minSize = childPosition(expandSize, minSize); 175 176 } 177 178 protected override void drawImpl(Rectangle, Rectangle area) { 179 180 assertClean(children, "Children were changed without calling updateSize()."); 181 182 auto position = Vector2(area.x, area.y); 183 184 foreach (child; filterChildren) { 185 186 // Get params 187 const size = childSpace(child, Vector2(area.width, area.height), true); 188 const rect = Rectangle( 189 position.x, position.y, 190 size.x, size.y 191 ); 192 193 // Draw the child 194 child.draw(rect); 195 196 // Offset position 197 if (directionHorizontal) position.x += cast(int) size.x; 198 else position.y += cast(int) size.y; 199 200 } 201 202 } 203 204 /// List children in the space, removing all nodes queued for deletion beforehand. 205 protected auto filterChildren() { 206 207 struct ChildIterator { 208 209 Space node; 210 211 int opApply(int delegate(Node) @safe fun) @trusted { 212 213 node.children.lock(); 214 scope (exit) node.children.unlock(); 215 216 size_t destinationIndex = 0; 217 218 // Iterate through all children. When we come upon ones that are queued for deletion, 219 foreach (sourceIndex, child; node.children) { 220 221 const toRemove = child.toRemove; 222 child.toRemove = false; 223 224 // Ignore children that are to be removed 225 if (toRemove) continue; 226 227 // Yield the child 228 const status = fun(child); 229 230 // Move the child if needed 231 if (sourceIndex != destinationIndex) { 232 233 node.children.forceMutable[destinationIndex] = child; 234 235 } 236 237 // Stop iteration if requested 238 else if (status) return status; 239 240 // Set space for next nodes 241 destinationIndex++; 242 243 244 } 245 246 // Adjust length 247 node.children.forceMutable.length = destinationIndex; 248 249 return 0; 250 251 } 252 253 } 254 255 return ChildIterator(this); 256 257 } 258 259 /// Iterate over every child and perform the painting function. Will automatically remove nodes queued for removal. 260 /// Returns: An iterator that goes over all nodes. 261 deprecated("Use filterChildren instead") 262 protected void drawChildren(void delegate(Node) @safe painter) { 263 264 Node[] leftovers; 265 266 children.lock(); 267 scope (exit) children.unlock(); 268 269 // Draw each child and get rid of removed children 270 auto range = children[] 271 272 // Check if the node is queued for removal 273 .filter!((node) { 274 const status = node.toRemove; 275 node.toRemove = false; 276 return !status; 277 }) 278 279 // Draw the node 280 .tee!((node) => painter(node)); 281 282 // Do what we ought to do 283 () @trusted { 284 285 // Process the children and move them back to the original array 286 auto leftovers = range.moveAll(children.forceMutable); 287 288 // Adjust the array size 289 children.forceMutable.length -= leftovers.length; 290 291 }(); 292 293 } 294 295 protected override bool hoveredImpl(Rectangle, Vector2) const { 296 297 return false; 298 299 } 300 301 protected override inout(Style) pickStyle() inout { 302 303 return null; 304 305 } 306 307 /// Params: 308 /// child = Child size to add. 309 /// previous = Previous position. 310 private Vector2 childPosition(Vector2 child, Vector2 previous) const { 311 312 import std.algorithm : max; 313 314 // Horizontal 315 if (directionHorizontal) { 316 317 return Vector2( 318 previous.x + child.x, 319 max(minSize.y, child.y), 320 ); 321 322 } 323 324 // Vertical 325 else return Vector2( 326 max(minSize.x, child.x), 327 previous.y + child.y, 328 ); 329 330 } 331 332 /// Get space for a child. 333 /// Params: 334 /// child = Child to place 335 /// available = Available space 336 private Vector2 childSpace(const Node child, Vector2 available, bool stateful) const 337 in( 338 child.isHidden || child.layout.expand <= denominator, 339 format!"Nodes %s/%s sizes are out of date, call updateSize after updating the tree or layout (%s/%s)"( 340 typeid(this), typeid(child), child.layout.expand, denominator, 341 ) 342 ) 343 out( 344 r; [r.tupleof].all!isFinite, 345 format!"space: child %s given invalid size %s. available = %s, expand = %s, denominator = %s, reserved = %s"( 346 typeid(child), r, available, child.layout.expand, denominator, reservedSpace 347 ) 348 ) 349 do { 350 351 // Hidden, give it no space 352 if (child.isHidden) return Vector2(); 353 354 // Horizontal 355 if (directionHorizontal) { 356 357 const avail = (available.x - reservedSpace); 358 const minSize = stateful 359 ? child.minSize.x 360 : available.x; 361 362 return Vector2( 363 child.layout.expand 364 ? avail * child.layout.expand / denominator 365 : minSize, 366 available.y, 367 ); 368 369 } 370 371 // Vertical 372 else { 373 374 const avail = (available.y - reservedSpace); 375 const minSize = stateful 376 ? child.minSize.y 377 : available.y; 378 379 return Vector2( 380 available.x, 381 child.layout.expand 382 ? avail * child.layout.expand / denominator 383 : minSize, 384 ); 385 386 } 387 388 } 389 390 } 391 392 /// 393 unittest { 394 395 import fluid; 396 397 // A vspace will align all its content in a column 398 vspace( 399 label("First entry"), 400 label("Second entry"), 401 label("Third entry"), 402 ); 403 404 // hspace will lay out the nodes in a row 405 hspace( 406 label("One, "), 407 label("Two, "), 408 label("Three!"), 409 ); 410 411 // Combine them to quickly build layouts! 412 vspace( 413 label("Are you sure you want to proceed?"), 414 hspace( 415 button("Yes", delegate { }), 416 button("Cancel", delegate { }), 417 ), 418 ); 419 420 } 421 422 unittest { 423 424 class Square : Node { 425 426 mixin implHoveredRect; 427 428 Color color; 429 430 this(Color color) { 431 this.color = color; 432 } 433 434 override void resizeImpl(Vector2) { 435 minSize = Vector2(50, 50); 436 } 437 438 override void drawImpl(Rectangle, Rectangle inner) { 439 io.drawRectangle(inner, this.color); 440 } 441 442 } 443 444 auto io = new HeadlessBackend; 445 auto root = vspace( 446 new Square(color!"000"), 447 new Square(color!"001"), 448 new Square(color!"002"), 449 hspace( 450 new Square(color!"010"), 451 new Square(color!"011"), 452 new Square(color!"012"), 453 ), 454 ); 455 456 root.io = io; 457 root.theme = nullTheme; 458 root.draw(); 459 460 // vspace 461 io.assertRectangle(Rectangle(0, 0, 50, 50), color!"000"); 462 io.assertRectangle(Rectangle(0, 50, 50, 50), color!"001"); 463 io.assertRectangle(Rectangle(0, 100, 50, 50), color!"002"); 464 465 // hspace 466 io.assertRectangle(Rectangle( 0, 150, 50, 50), color!"010"); 467 io.assertRectangle(Rectangle( 50, 150, 50, 50), color!"011"); 468 io.assertRectangle(Rectangle(100, 150, 50, 50), color!"012"); 469 470 } 471 472 unittest { 473 474 import fluid.frame; 475 import fluid.structs; 476 477 auto io = new HeadlessBackend; 478 auto root = hspace( 479 layout!"fill", 480 vframe(layout!1), 481 vframe(layout!2), 482 vframe(layout!1), 483 ); 484 485 root.io = io; 486 root.theme = nullTheme.makeTheme!q{ 487 Frame.styleAdd.backgroundColor = color!"7d9"; 488 }; 489 490 // Frame 1 491 { 492 root.draw(); 493 io.assertRectangle(Rectangle(0, 0, 0, 0), color!"7d9"); 494 io.assertRectangle(Rectangle(200, 0, 0, 0), color!"7d9"); 495 io.assertRectangle(Rectangle(600, 0, 0, 0), color!"7d9"); 496 } 497 498 // Fill all nodes 499 foreach (child; root.children) { 500 child.layout.nodeAlign = NodeAlign.fill; 501 } 502 root.updateSize(); 503 504 { 505 io.nextFrame; 506 root.draw(); 507 io.assertRectangle(Rectangle( 0, 0, 200, 600), color!"7d9"); 508 io.assertRectangle(Rectangle(200, 0, 400, 600), color!"7d9"); 509 io.assertRectangle(Rectangle(600, 0, 200, 600), color!"7d9"); 510 } 511 512 const alignments = [NodeAlign.start, NodeAlign.center, NodeAlign.end]; 513 514 // Make Y alignment different across all three 515 foreach (pair; root.children.zip(alignments)) { 516 pair[0].layout.nodeAlign = pair[1]; 517 } 518 519 { 520 io.nextFrame; 521 root.draw(); 522 io.assertRectangle(Rectangle( 0, 0, 0, 0), color!"7d9"); 523 io.assertRectangle(Rectangle(400, 300, 0, 0), color!"7d9"); 524 io.assertRectangle(Rectangle(800, 600, 0, 0), color!"7d9"); 525 } 526 527 } 528 529 unittest { 530 531 import fluid.frame; 532 import fluid.structs; 533 534 auto io = new HeadlessBackend(Vector2(270, 270)); 535 auto root = hframe( 536 layout!"fill", 537 vspace(layout!2), 538 vframe( 539 layout!(1, "fill"), 540 hspace(layout!2), 541 hframe( 542 layout!(1, "fill"), 543 vframe( 544 layout!(1, "fill"), 545 hframe( 546 layout!(1, "fill") 547 ), 548 hspace(layout!2), 549 ), 550 vspace(layout!2), 551 ) 552 ), 553 ); 554 555 root.theme = nullTheme.makeTheme!q{ 556 Frame.styleAdd.backgroundColor = color!"0004"; 557 }; 558 root.io = io; 559 root.draw(); 560 561 io.assertRectangle(Rectangle( 0, 0, 270, 270), color!"0004"); 562 io.assertRectangle(Rectangle(180, 0, 90, 270), color!"0004"); 563 io.assertRectangle(Rectangle(180, 180, 90, 90), color!"0004"); 564 io.assertRectangle(Rectangle(180, 180, 30, 90), color!"0004"); 565 io.assertRectangle(Rectangle(180, 180, 30, 30), color!"0004"); 566 567 }