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 16 17 @safe: 18 19 20 /// This is a space, a basic container for other nodes. 21 /// 22 /// Nodes are laid in a column (`vframe`) or in a row (`hframe`). 23 /// 24 /// Space only acts as a container and doesn't implement styles and doesn't take focus. It's very useful as a helper for 25 /// building layout, while `Frame` remains to provide styling. 26 alias vspace = simpleConstructor!Space; 27 28 /// ditto 29 alias hspace = simpleConstructor!(Space, (a) { 30 31 a.directionHorizontal = true; 32 33 }); 34 35 /// ditto 36 class Space : Node { 37 38 public { 39 40 /// Children of this frame. 41 Children children; 42 43 /// Defines in what directions children of this frame should be placed. 44 /// 45 /// If true, children are placed horizontally, if false, vertically. 46 bool isHorizontal; 47 48 alias horizontal = isHorizontal; 49 alias directionHorizontal = horizontal; 50 51 } 52 53 private { 54 55 /// Denominator for content sizing. 56 uint denominator; 57 58 /// Space reserved for shrinking elements. 59 float reservedSpace; 60 61 } 62 63 /// Create the space and fill it with given nodes. 64 this(Node[] nodes...) { 65 66 this.children ~= nodes; 67 68 } 69 70 /// Create the space using nodes from the given range. 71 this(Range)(Range range) 72 if (isInputRange!Range) 73 do { 74 75 this.children ~= range; 76 77 } 78 79 /// Add children. 80 pragma(inline, true) 81 void opOpAssign(string operator : "~", T)(T nodes) { 82 83 children ~= nodes; 84 85 } 86 87 protected override void resizeImpl(Vector2 available) { 88 89 import std.algorithm : max, map, fold; 90 91 // Now that we're recalculating the layout, we can remove the dirty flag from children 92 children.clearDirty; 93 94 // Reset size 95 minSize = Vector2(0, 0); 96 reservedSpace = 0; 97 denominator = 0; 98 99 // Ignore the rest if there's no children 100 if (!children.length) return; 101 102 Vector2 maxExpandSize; 103 104 // Collect expanding children in a separate array 105 Node[] expandChildren; 106 size_t visibleChildren; 107 foreach (child; children) { 108 109 visibleChildren += !child.isHidden; 110 111 // This node expands and isn't hidden 112 if (child.layout.expand && !child.isHidden) { 113 114 // Make it happen later 115 expandChildren ~= child; 116 117 // Add to the denominator 118 denominator += child.layout.expand; 119 120 } 121 122 // Check non-expand nodes now 123 else { 124 125 child.resize(tree, theme, childSpace(child, available, false)); 126 minSize = addSize(child.minSize, minSize); 127 128 // Reserve space for this node 129 reservedSpace += directionHorizontal 130 ? child.minSize.x 131 : child.minSize.y; 132 133 } 134 135 } 136 137 const gapSpace 138 = visibleChildren == 0 ? 0 139 : isHorizontal ? style.gap.sideX * (visibleChildren - 1u) 140 : style.gap.sideY * (visibleChildren - 1u); 141 142 // Reserve space for gaps 143 reservedSpace += gapSpace; 144 145 if (isHorizontal) 146 minSize.x += gapSpace; 147 else 148 minSize.y += gapSpace; 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 = addSize(expandSize, minSize); 175 176 } 177 178 unittest { 179 180 import std.meta; 181 import fluid.frame; 182 import fluid.size_lock; 183 184 auto theme = nullTheme.derive( 185 rule!Space( 186 Rule.gap = 12, 187 ), 188 ); 189 190 auto root = vspace( 191 theme, 192 sizeLock!vframe( 193 sizeLimitY = 200 194 ), 195 sizeLock!vframe( 196 sizeLimitY = 200 197 ), 198 sizeLock!vframe( 199 sizeLimitY = 200 200 ), 201 ); 202 root.draw(); 203 204 assert(isClose(root.minSize.y, 200 * 3 + 12 * 2)); 205 206 } 207 208 protected override void drawImpl(Rectangle, Rectangle area) { 209 210 assertClean(children, "Children were changed without calling updateSize()."); 211 212 auto position = start(area); 213 214 foreach (child; filterChildren) { 215 216 // Ignore if this child is not visible 217 if (child.isHidden) continue; 218 219 // Get params 220 const size = childSpace(child, size(area), true); 221 const rect = Rectangle( 222 position.x, position.y, 223 size.x, size.y 224 ); 225 226 // Draw the child 227 child.draw(rect); 228 229 // Offset position 230 position = childOffset(position, size); 231 232 } 233 234 } 235 236 /// List children in the space, removing all nodes queued for deletion beforehand. 237 protected auto filterChildren() { 238 239 struct ChildIterator { 240 241 Space node; 242 243 int opApply(int delegate(Node) @safe fun) @trusted { 244 245 foreach (_, node; this) { 246 247 if (auto result = fun(node)) { 248 return result; 249 } 250 251 } 252 return 0; 253 254 } 255 256 int opApply(int delegate(size_t index, Node) @safe fun) @trusted { 257 258 node.children.lock(); 259 scope (exit) node.children.unlock(); 260 261 size_t destinationIndex = 0; 262 int end = 0; 263 264 // Iterate through all children. When we come upon ones that are queued for deletion, 265 foreach (sourceIndex, child; node.children) { 266 267 const toRemove = child.toRemove; 268 child.toRemove = false; 269 270 // Ignore children that are to be removed 271 if (toRemove) continue; 272 273 // Yield the child 274 if (!end) 275 end = fun(destinationIndex, child); 276 277 // Move the child if needed 278 if (sourceIndex != destinationIndex) { 279 280 node.children.forceMutable[destinationIndex] = child; 281 282 } 283 284 // Stop iteration if requested — and if there's nothing to move 285 else if (end) return end; 286 287 // Set space for next nodes 288 destinationIndex++; 289 290 291 } 292 293 // Adjust length 294 node.children.forceMutable.length = destinationIndex; 295 296 return 0; 297 298 } 299 300 } 301 302 return ChildIterator(this); 303 304 } 305 306 /// Space does not take hover; isHovered is always false. 307 protected override bool hoveredImpl(Rectangle, Vector2) { 308 309 return false; 310 311 } 312 313 /// Params: 314 /// child = Child size to add. 315 /// previous = Previous position. 316 private Vector2 addSize(Vector2 child, Vector2 previous) const { 317 318 import std.algorithm : max; 319 320 // Horizontal 321 if (directionHorizontal) { 322 323 return Vector2( 324 previous.x + child.x, 325 max(minSize.y, child.y), 326 ); 327 328 } 329 330 // Vertical 331 else return Vector2( 332 max(minSize.x, child.x), 333 previous.y + child.y, 334 ); 335 336 } 337 338 /// Calculate the offset for the next node, given the `childSpace` result for its previous sibling. 339 protected Vector2 childOffset(Vector2 currentOffset, Vector2 childSpace) { 340 341 if (isHorizontal) 342 return currentOffset + Vector2(childSpace.x + style.gap.sideX, 0); 343 else 344 return currentOffset + Vector2(0, childSpace.y + style.gap.sideY); 345 346 } 347 348 /// Get space for a child. 349 /// Params: 350 /// child = Child to place 351 /// available = Available space 352 protected Vector2 childSpace(const Node child, Vector2 available, bool stateful = true) const 353 in( 354 child.isHidden || child.layout.expand <= denominator, 355 format!"Nodes %s/%s sizes are out of date, call updateSize after updating the tree or layout (%s/%s)"( 356 typeid(this), typeid(child), child.layout.expand, denominator, 357 ) 358 ) 359 out( 360 r; [r.tupleof].all!isFinite, 361 format!"space: child %s given invalid size %s. available = %s, expand = %s, denominator = %s, reserved = %s"( 362 typeid(child), r, available, child.layout.expand, denominator, reservedSpace 363 ) 364 ) 365 do { 366 367 // Hidden, give it no space 368 if (child.isHidden) return Vector2(); 369 370 // Horizontal 371 if (directionHorizontal) { 372 373 const avail = (available.x - reservedSpace); 374 const minSize = stateful 375 ? child.minSize.x 376 : available.x; 377 378 return Vector2( 379 child.layout.expand 380 ? avail * child.layout.expand / denominator 381 : minSize, 382 available.y, 383 ); 384 385 } 386 387 // Vertical 388 else { 389 390 const avail = (available.y - reservedSpace); 391 const minSize = stateful 392 ? child.minSize.y 393 : available.y; 394 395 return Vector2( 396 available.x, 397 child.layout.expand 398 ? avail * child.layout.expand / denominator 399 : minSize, 400 ); 401 402 } 403 404 } 405 406 } 407 408 /// 409 unittest { 410 411 import fluid; 412 413 // A vspace will align all its content in a column 414 vspace( 415 label("First entry"), 416 label("Second entry"), 417 label("Third entry"), 418 ); 419 420 // hspace will lay out the nodes in a row 421 hspace( 422 label("One, "), 423 label("Two, "), 424 label("Three!"), 425 ); 426 427 // Combine them to quickly build layouts! 428 vspace( 429 label("Are you sure you want to proceed?"), 430 hspace( 431 button("Yes", delegate { }), 432 button("Cancel", delegate { }), 433 ), 434 ); 435 436 } 437 438 unittest { 439 440 class Square : Node { 441 442 Color color; 443 444 this(Color color) { 445 this.color = color; 446 } 447 448 override void resizeImpl(Vector2) { 449 minSize = Vector2(50, 50); 450 } 451 452 override void drawImpl(Rectangle, Rectangle inner) { 453 io.drawRectangle(inner, this.color); 454 } 455 456 } 457 458 auto io = new HeadlessBackend; 459 auto root = vspace( 460 new Square(color!"000"), 461 new Square(color!"001"), 462 new Square(color!"002"), 463 hspace( 464 new Square(color!"010"), 465 new Square(color!"011"), 466 new Square(color!"012"), 467 ), 468 ); 469 470 root.io = io; 471 root.theme = nullTheme; 472 root.draw(); 473 474 // vspace 475 io.assertRectangle(Rectangle(0, 0, 50, 50), color!"000"); 476 io.assertRectangle(Rectangle(0, 50, 50, 50), color!"001"); 477 io.assertRectangle(Rectangle(0, 100, 50, 50), color!"002"); 478 479 // hspace 480 io.assertRectangle(Rectangle( 0, 150, 50, 50), color!"010"); 481 io.assertRectangle(Rectangle( 50, 150, 50, 50), color!"011"); 482 io.assertRectangle(Rectangle(100, 150, 50, 50), color!"012"); 483 484 } 485 486 unittest { 487 488 import fluid.frame; 489 import fluid.structs; 490 491 auto io = new HeadlessBackend; 492 auto root = hspace( 493 layout!"fill", 494 vframe(layout!1), 495 vframe(layout!2), 496 vframe(layout!1), 497 ); 498 499 with (Rule) 500 root.theme = nullTheme.derive( 501 rule!Frame(backgroundColor = color!"7d9"), 502 ); 503 root.io = io; 504 505 // Frame 1 506 { 507 root.draw(); 508 io.assertRectangle(Rectangle(0, 0, 0, 0), color!"7d9"); 509 io.assertRectangle(Rectangle(200, 0, 0, 0), color!"7d9"); 510 io.assertRectangle(Rectangle(600, 0, 0, 0), color!"7d9"); 511 } 512 513 // Fill all nodes 514 foreach (child; root.children) { 515 child.layout.nodeAlign = NodeAlign.fill; 516 } 517 root.updateSize(); 518 519 { 520 io.nextFrame; 521 root.draw(); 522 io.assertRectangle(Rectangle( 0, 0, 200, 600), color!"7d9"); 523 io.assertRectangle(Rectangle(200, 0, 400, 600), color!"7d9"); 524 io.assertRectangle(Rectangle(600, 0, 200, 600), color!"7d9"); 525 } 526 527 const alignments = [NodeAlign.start, NodeAlign.center, NodeAlign.end]; 528 529 // Make Y alignment different across all three 530 foreach (pair; root.children.zip(alignments)) { 531 pair[0].layout.nodeAlign = pair[1]; 532 } 533 534 { 535 io.nextFrame; 536 root.draw(); 537 io.assertRectangle(Rectangle( 0, 0, 0, 0), color!"7d9"); 538 io.assertRectangle(Rectangle(400, 300, 0, 0), color!"7d9"); 539 io.assertRectangle(Rectangle(800, 600, 0, 0), color!"7d9"); 540 } 541 542 } 543 544 unittest { 545 546 import fluid.frame; 547 import fluid.structs; 548 549 auto io = new HeadlessBackend(Vector2(270, 270)); 550 auto root = hframe( 551 layout!"fill", 552 vspace(layout!2), 553 vframe( 554 layout!(1, "fill"), 555 hspace(layout!2), 556 hframe( 557 layout!(1, "fill"), 558 vframe( 559 layout!(1, "fill"), 560 hframe( 561 layout!(1, "fill") 562 ), 563 hspace(layout!2), 564 ), 565 vspace(layout!2), 566 ) 567 ), 568 ); 569 570 with (Rule) 571 root.theme = nullTheme.derive( 572 rule!Frame(backgroundColor = color!"0004"), 573 ); 574 root.io = io; 575 root.draw(); 576 577 io.assertRectangle(Rectangle( 0, 0, 270, 270), color!"0004"); 578 io.assertRectangle(Rectangle(180, 0, 90, 270), color!"0004"); 579 io.assertRectangle(Rectangle(180, 180, 90, 90), color!"0004"); 580 io.assertRectangle(Rectangle(180, 180, 30, 90), color!"0004"); 581 io.assertRectangle(Rectangle(180, 180, 30, 30), color!"0004"); 582 583 } 584 585 // https://git.samerion.com/Samerion/Fluid/issues/58 586 unittest { 587 588 import fluid.frame; 589 import fluid.label; 590 import fluid.structs; 591 592 auto fill = layout!(1, "fill"); 593 auto io = new HeadlessBackend; 594 auto myTheme = nullTheme.derive( 595 rule!Frame(Rule.backgroundColor = color!"#303030"), 596 rule!Label(Rule.backgroundColor = color!"#e65bb8"), 597 ); 598 auto root = hframe( 599 fill, 600 myTheme, 601 label(fill, "1"), 602 label(fill, "2"), 603 label(fill, "3"), 604 label(fill, "4"), 605 label(fill, "5"), 606 label(fill, "6"), 607 label(fill, "7"), 608 label(fill, "8"), 609 label(fill, "9"), 610 label(fill, "10"), 611 label(fill, "11"), 612 label(fill, "12"), 613 ); 614 615 root.io = io; 616 root.draw(); 617 618 io.assertRectangle(Rectangle( 0*800/12f, 0, 66.66, 600), color!"#e65bb8"); 619 io.assertRectangle(Rectangle( 1*800/12f, 0, 66.66, 600), color!"#e65bb8"); 620 io.assertRectangle(Rectangle( 2*800/12f, 0, 66.66, 600), color!"#e65bb8"); 621 io.assertRectangle(Rectangle( 3*800/12f, 0, 66.66, 600), color!"#e65bb8"); 622 io.assertRectangle(Rectangle( 4*800/12f, 0, 66.66, 600), color!"#e65bb8"); 623 io.assertRectangle(Rectangle( 5*800/12f, 0, 66.66, 600), color!"#e65bb8"); 624 io.assertRectangle(Rectangle( 6*800/12f, 0, 66.66, 600), color!"#e65bb8"); 625 io.assertRectangle(Rectangle( 7*800/12f, 0, 66.66, 600), color!"#e65bb8"); 626 io.assertRectangle(Rectangle( 8*800/12f, 0, 66.66, 600), color!"#e65bb8"); 627 io.assertRectangle(Rectangle( 9*800/12f, 0, 66.66, 600), color!"#e65bb8"); 628 io.assertRectangle(Rectangle(10*800/12f, 0, 66.66, 600), color!"#e65bb8"); 629 io.assertRectangle(Rectangle(11*800/12f, 0, 66.66, 600), color!"#e65bb8"); 630 631 } 632 633 unittest { 634 635 import fluid.frame; 636 import fluid.theme; 637 import fluid.structs : layout; 638 639 auto io = new HeadlessBackend; 640 auto theme = nullTheme.derive( 641 rule!Space( 642 gap = 4, 643 ), 644 rule!Frame( 645 backgroundColor = color("#f00"), 646 ), 647 ); 648 auto root = vspace( 649 layout!"fill", 650 theme, 651 vframe(layout!(1, "fill")), 652 vframe(layout!(1, "fill")), 653 vframe(layout!(1, "fill")), 654 vframe(layout!(1, "fill")), 655 ); 656 657 root.io = io; 658 root.draw(); 659 660 io.assertRectangle(Rectangle(0, 0, 800, 147), color("#f00")); 661 io.assertRectangle(Rectangle(0, 151, 800, 147), color("#f00")); 662 io.assertRectangle(Rectangle(0, 302, 800, 147), color("#f00")); 663 io.assertRectangle(Rectangle(0, 453, 800, 147), color("#f00")); 664 665 } 666 667 @("Gaps do not apply to invisible children") 668 unittest { 669 670 import fluid.theme; 671 672 auto theme = nullTheme.derive( 673 rule!Space(gap = 4), 674 ); 675 676 auto spy = new class Space { 677 678 Vector2 position; 679 680 override void drawImpl(Rectangle outer, Rectangle inner) { 681 682 position = outer.start; 683 684 } 685 686 }; 687 688 auto root = vspace( 689 theme, 690 hspace(), 691 hspace(), 692 hspace(), 693 spy, 694 ); 695 696 root.draw(); 697 698 assert(spy.position == Vector2(0, 12)); 699 700 // Hide one child 701 root.children[0].hide(); 702 root.draw(); 703 704 assert(spy.position == Vector2(0, 8)); 705 706 707 } 708 709 @("applied style.gap depends on axis") 710 unittest { 711 712 auto theme = nullTheme.derive( 713 rule!Space( 714 Rule.gap = [2, 4], 715 ), 716 ); 717 718 class Warden : Space { 719 720 Rectangle outer; 721 722 override void drawImpl(Rectangle outer, Rectangle inner) { 723 super.drawImpl(this.outer = outer, inner); 724 } 725 726 } 727 728 Warden[4] wardens; 729 730 auto root = vspace( 731 theme, 732 hspace( 733 wardens[0] = new Warden, 734 wardens[1] = new Warden, 735 ), 736 vspace( 737 wardens[2] = new Warden, 738 wardens[3] = new Warden, 739 ), 740 ); 741 742 root.draw(); 743 744 assert(wardens[0].outer.start == Vector2(0, 0)); 745 assert(wardens[1].outer.start == Vector2(2, 0)); 746 assert(wardens[2].outer.start == Vector2(0, 4)); 747 assert(wardens[3].outer.start == Vector2(0, 8)); 748 749 }