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 foreach (child; children) { 107 108 // This node expands and isn't hidden 109 if (child.layout.expand && !child.isHidden) { 110 111 // Make it happen later 112 expandChildren ~= child; 113 114 // Add to the denominator 115 denominator += child.layout.expand; 116 117 } 118 119 // Check non-expand nodes now 120 else { 121 122 child.resize(tree, theme, childSpace(child, available, false)); 123 minSize = addSize(child.minSize, minSize); 124 125 // Reserve space for this node 126 reservedSpace += directionHorizontal 127 ? child.minSize.x 128 : child.minSize.y; 129 130 } 131 132 } 133 134 const gapSpace = style.gap * (children.length - 1); 135 136 // Reserve space for gaps 137 reservedSpace += gapSpace; 138 139 if (isHorizontal) 140 minSize.x += gapSpace; 141 else 142 minSize.y += gapSpace; 143 144 // Calculate the size of expanding children last 145 foreach (child; expandChildren) { 146 147 // Resize the child 148 child.resize(tree, theme, childSpace(child, available, false)); 149 150 const childSize = child.minSize; 151 const childExpand = child.layout.expand; 152 153 const segmentSize = horizontal 154 ? Vector2(childSize.x / childExpand, childSize.y) 155 : Vector2(childSize.x, childSize.y / childExpand); 156 157 // Reserve expand space 158 maxExpandSize.x = max(maxExpandSize.x, segmentSize.x); 159 maxExpandSize.y = max(maxExpandSize.y, segmentSize.y); 160 161 } 162 163 const expandSize = horizontal 164 ? Vector2(maxExpandSize.x * denominator, maxExpandSize.y) 165 : Vector2(maxExpandSize.x, maxExpandSize.y * denominator); 166 167 // Add the expand space 168 minSize = addSize(expandSize, minSize); 169 170 } 171 172 unittest { 173 174 import std.meta; 175 import fluid.frame; 176 import fluid.size_lock; 177 178 auto theme = nullTheme.derive( 179 rule!Space( 180 Rule.gap = 12, 181 ), 182 ); 183 184 auto root = vspace( 185 theme, 186 sizeLock!vframe( 187 sizeLimitY = 200 188 ), 189 sizeLock!vframe( 190 sizeLimitY = 200 191 ), 192 sizeLock!vframe( 193 sizeLimitY = 200 194 ), 195 ); 196 root.draw(); 197 198 assert(isClose(root.minSize.y, 200 * 3 + 12 * 2)); 199 200 } 201 202 protected override void drawImpl(Rectangle, Rectangle area) { 203 204 assertClean(children, "Children were changed without calling updateSize()."); 205 206 auto position = start(area); 207 208 foreach (child; filterChildren) { 209 210 // Get params 211 const size = childSpace(child, size(area), true); 212 const rect = Rectangle( 213 position.x, position.y, 214 size.x, size.y 215 ); 216 217 // Draw the child 218 child.draw(rect); 219 220 // Offset position 221 position = childOffset(position, size); 222 223 } 224 225 } 226 227 /// List children in the space, removing all nodes queued for deletion beforehand. 228 protected auto filterChildren() { 229 230 struct ChildIterator { 231 232 Space node; 233 234 int opApply(int delegate(Node) @safe fun) @trusted { 235 236 node.children.lock(); 237 scope (exit) node.children.unlock(); 238 239 size_t destinationIndex = 0; 240 241 // Iterate through all children. When we come upon ones that are queued for deletion, 242 foreach (sourceIndex, child; node.children) { 243 244 const toRemove = child.toRemove; 245 child.toRemove = false; 246 247 // Ignore children that are to be removed 248 if (toRemove) continue; 249 250 // Yield the child 251 const status = fun(child); 252 253 // Move the child if needed 254 if (sourceIndex != destinationIndex) { 255 256 node.children.forceMutable[destinationIndex] = child; 257 258 } 259 260 // Stop iteration if requested 261 else if (status) return status; 262 263 // Set space for next nodes 264 destinationIndex++; 265 266 267 } 268 269 // Adjust length 270 node.children.forceMutable.length = destinationIndex; 271 272 return 0; 273 274 } 275 276 } 277 278 return ChildIterator(this); 279 280 } 281 282 /// Space does not take hover; isHovered is always false. 283 protected override bool hoveredImpl(Rectangle, Vector2) { 284 285 return false; 286 287 } 288 289 /// Params: 290 /// child = Child size to add. 291 /// previous = Previous position. 292 private Vector2 addSize(Vector2 child, Vector2 previous) const { 293 294 import std.algorithm : max; 295 296 // Horizontal 297 if (directionHorizontal) { 298 299 return Vector2( 300 previous.x + child.x, 301 max(minSize.y, child.y), 302 ); 303 304 } 305 306 // Vertical 307 else return Vector2( 308 max(minSize.x, child.x), 309 previous.y + child.y, 310 ); 311 312 } 313 314 /// Calculate the offset for the next node, given the `childSpace` result for its previous sibling. 315 protected Vector2 childOffset(Vector2 currentOffset, Vector2 childSpace) { 316 317 if (isHorizontal) 318 return currentOffset + Vector2(childSpace.x + style.gap, 0); 319 else 320 return currentOffset + Vector2(0, childSpace.y + style.gap); 321 322 } 323 324 /// Get space for a child. 325 /// Params: 326 /// child = Child to place 327 /// available = Available space 328 protected Vector2 childSpace(const Node child, Vector2 available, bool stateful = true) const 329 in( 330 child.isHidden || child.layout.expand <= denominator, 331 format!"Nodes %s/%s sizes are out of date, call updateSize after updating the tree or layout (%s/%s)"( 332 typeid(this), typeid(child), child.layout.expand, denominator, 333 ) 334 ) 335 out( 336 r; [r.tupleof].all!isFinite, 337 format!"space: child %s given invalid size %s. available = %s, expand = %s, denominator = %s, reserved = %s"( 338 typeid(child), r, available, child.layout.expand, denominator, reservedSpace 339 ) 340 ) 341 do { 342 343 // Hidden, give it no space 344 if (child.isHidden) return Vector2(); 345 346 // Horizontal 347 if (directionHorizontal) { 348 349 const avail = (available.x - reservedSpace); 350 const minSize = stateful 351 ? child.minSize.x 352 : available.x; 353 354 return Vector2( 355 child.layout.expand 356 ? avail * child.layout.expand / denominator 357 : minSize, 358 available.y, 359 ); 360 361 } 362 363 // Vertical 364 else { 365 366 const avail = (available.y - reservedSpace); 367 const minSize = stateful 368 ? child.minSize.y 369 : available.y; 370 371 return Vector2( 372 available.x, 373 child.layout.expand 374 ? avail * child.layout.expand / denominator 375 : minSize, 376 ); 377 378 } 379 380 } 381 382 } 383 384 /// 385 unittest { 386 387 import fluid; 388 389 // A vspace will align all its content in a column 390 vspace( 391 label("First entry"), 392 label("Second entry"), 393 label("Third entry"), 394 ); 395 396 // hspace will lay out the nodes in a row 397 hspace( 398 label("One, "), 399 label("Two, "), 400 label("Three!"), 401 ); 402 403 // Combine them to quickly build layouts! 404 vspace( 405 label("Are you sure you want to proceed?"), 406 hspace( 407 button("Yes", delegate { }), 408 button("Cancel", delegate { }), 409 ), 410 ); 411 412 } 413 414 unittest { 415 416 class Square : Node { 417 418 Color color; 419 420 this(Color color) { 421 this.color = color; 422 } 423 424 override void resizeImpl(Vector2) { 425 minSize = Vector2(50, 50); 426 } 427 428 override void drawImpl(Rectangle, Rectangle inner) { 429 io.drawRectangle(inner, this.color); 430 } 431 432 } 433 434 auto io = new HeadlessBackend; 435 auto root = vspace( 436 new Square(color!"000"), 437 new Square(color!"001"), 438 new Square(color!"002"), 439 hspace( 440 new Square(color!"010"), 441 new Square(color!"011"), 442 new Square(color!"012"), 443 ), 444 ); 445 446 root.io = io; 447 root.theme = nullTheme; 448 root.draw(); 449 450 // vspace 451 io.assertRectangle(Rectangle(0, 0, 50, 50), color!"000"); 452 io.assertRectangle(Rectangle(0, 50, 50, 50), color!"001"); 453 io.assertRectangle(Rectangle(0, 100, 50, 50), color!"002"); 454 455 // hspace 456 io.assertRectangle(Rectangle( 0, 150, 50, 50), color!"010"); 457 io.assertRectangle(Rectangle( 50, 150, 50, 50), color!"011"); 458 io.assertRectangle(Rectangle(100, 150, 50, 50), color!"012"); 459 460 } 461 462 unittest { 463 464 import fluid.frame; 465 import fluid.structs; 466 467 auto io = new HeadlessBackend; 468 auto root = hspace( 469 layout!"fill", 470 vframe(layout!1), 471 vframe(layout!2), 472 vframe(layout!1), 473 ); 474 475 with (Rule) 476 root.theme = nullTheme.derive( 477 rule!Frame(backgroundColor = color!"7d9"), 478 ); 479 root.io = io; 480 481 // Frame 1 482 { 483 root.draw(); 484 io.assertRectangle(Rectangle(0, 0, 0, 0), color!"7d9"); 485 io.assertRectangle(Rectangle(200, 0, 0, 0), color!"7d9"); 486 io.assertRectangle(Rectangle(600, 0, 0, 0), color!"7d9"); 487 } 488 489 // Fill all nodes 490 foreach (child; root.children) { 491 child.layout.nodeAlign = NodeAlign.fill; 492 } 493 root.updateSize(); 494 495 { 496 io.nextFrame; 497 root.draw(); 498 io.assertRectangle(Rectangle( 0, 0, 200, 600), color!"7d9"); 499 io.assertRectangle(Rectangle(200, 0, 400, 600), color!"7d9"); 500 io.assertRectangle(Rectangle(600, 0, 200, 600), color!"7d9"); 501 } 502 503 const alignments = [NodeAlign.start, NodeAlign.center, NodeAlign.end]; 504 505 // Make Y alignment different across all three 506 foreach (pair; root.children.zip(alignments)) { 507 pair[0].layout.nodeAlign = pair[1]; 508 } 509 510 { 511 io.nextFrame; 512 root.draw(); 513 io.assertRectangle(Rectangle( 0, 0, 0, 0), color!"7d9"); 514 io.assertRectangle(Rectangle(400, 300, 0, 0), color!"7d9"); 515 io.assertRectangle(Rectangle(800, 600, 0, 0), color!"7d9"); 516 } 517 518 } 519 520 unittest { 521 522 import fluid.frame; 523 import fluid.structs; 524 525 auto io = new HeadlessBackend(Vector2(270, 270)); 526 auto root = hframe( 527 layout!"fill", 528 vspace(layout!2), 529 vframe( 530 layout!(1, "fill"), 531 hspace(layout!2), 532 hframe( 533 layout!(1, "fill"), 534 vframe( 535 layout!(1, "fill"), 536 hframe( 537 layout!(1, "fill") 538 ), 539 hspace(layout!2), 540 ), 541 vspace(layout!2), 542 ) 543 ), 544 ); 545 546 with (Rule) 547 root.theme = nullTheme.derive( 548 rule!Frame(backgroundColor = color!"0004"), 549 ); 550 root.io = io; 551 root.draw(); 552 553 io.assertRectangle(Rectangle( 0, 0, 270, 270), color!"0004"); 554 io.assertRectangle(Rectangle(180, 0, 90, 270), color!"0004"); 555 io.assertRectangle(Rectangle(180, 180, 90, 90), color!"0004"); 556 io.assertRectangle(Rectangle(180, 180, 30, 90), color!"0004"); 557 io.assertRectangle(Rectangle(180, 180, 30, 30), color!"0004"); 558 559 } 560 561 // https://git.samerion.com/Samerion/Fluid/issues/58 562 unittest { 563 564 import fluid.frame; 565 import fluid.label; 566 import fluid.structs; 567 568 auto fill = layout!(1, "fill"); 569 auto io = new HeadlessBackend; 570 auto myTheme = nullTheme.derive( 571 rule!Frame(Rule.backgroundColor = color!"#303030"), 572 rule!Label(Rule.backgroundColor = color!"#e65bb8"), 573 ); 574 auto root = hframe( 575 fill, 576 myTheme, 577 label(fill, "1"), 578 label(fill, "2"), 579 label(fill, "3"), 580 label(fill, "4"), 581 label(fill, "5"), 582 label(fill, "6"), 583 label(fill, "7"), 584 label(fill, "8"), 585 label(fill, "9"), 586 label(fill, "10"), 587 label(fill, "11"), 588 label(fill, "12"), 589 ); 590 591 root.io = io; 592 root.draw(); 593 594 io.assertRectangle(Rectangle( 0*800/12f, 0, 66.66, 600), color!"#e65bb8"); 595 io.assertRectangle(Rectangle( 1*800/12f, 0, 66.66, 600), color!"#e65bb8"); 596 io.assertRectangle(Rectangle( 2*800/12f, 0, 66.66, 600), color!"#e65bb8"); 597 io.assertRectangle(Rectangle( 3*800/12f, 0, 66.66, 600), color!"#e65bb8"); 598 io.assertRectangle(Rectangle( 4*800/12f, 0, 66.66, 600), color!"#e65bb8"); 599 io.assertRectangle(Rectangle( 5*800/12f, 0, 66.66, 600), color!"#e65bb8"); 600 io.assertRectangle(Rectangle( 6*800/12f, 0, 66.66, 600), color!"#e65bb8"); 601 io.assertRectangle(Rectangle( 7*800/12f, 0, 66.66, 600), color!"#e65bb8"); 602 io.assertRectangle(Rectangle( 8*800/12f, 0, 66.66, 600), color!"#e65bb8"); 603 io.assertRectangle(Rectangle( 9*800/12f, 0, 66.66, 600), color!"#e65bb8"); 604 io.assertRectangle(Rectangle(10*800/12f, 0, 66.66, 600), color!"#e65bb8"); 605 io.assertRectangle(Rectangle(11*800/12f, 0, 66.66, 600), color!"#e65bb8"); 606 607 } 608 609 unittest { 610 611 import fluid.frame; 612 import fluid.theme; 613 import fluid.structs : layout; 614 615 auto io = new HeadlessBackend; 616 auto theme = nullTheme.derive( 617 rule!Space( 618 gap = 4, 619 ), 620 rule!Frame( 621 backgroundColor = color("#f00"), 622 ), 623 ); 624 auto root = vspace( 625 layout!"fill", 626 theme, 627 vframe(layout!(1, "fill")), 628 vframe(layout!(1, "fill")), 629 vframe(layout!(1, "fill")), 630 vframe(layout!(1, "fill")), 631 ); 632 633 root.io = io; 634 root.draw(); 635 636 io.assertRectangle(Rectangle(0, 0, 800, 147), color("#f00")); 637 io.assertRectangle(Rectangle(0, 151, 800, 147), color("#f00")); 638 io.assertRectangle(Rectangle(0, 302, 800, 147), color("#f00")); 639 io.assertRectangle(Rectangle(0, 453, 800, 147), color("#f00")); 640 641 }