1 module fluid.grid; 2 3 import std.range; 4 import std.algorithm; 5 6 import fluid.node; 7 import fluid.tree; 8 import fluid.frame; 9 import fluid.style; 10 import fluid.utils; 11 import fluid.backend; 12 import fluid.structs; 13 14 15 @safe: 16 17 18 deprecated("`Grid` and `grid` were renamed to `GridFrame` and `gridFrame` respectively. To be removed in 0.8.0.") { 19 20 alias grid = simpleConstructor!GridFrame; 21 alias Grid = GridFrame; 22 23 } 24 25 alias gridFrame = simpleConstructor!GridFrame; 26 alias gridRow = simpleConstructor!GridRow; 27 28 // TODO rename segments to columns? 29 30 /// Segments is used to set the number of columns spanned by a grid item. When applied to a grid, it sets the number of 31 /// columns the grid will have. 32 struct Segments { 33 34 /// Number of columns used by a grid item. 35 uint amount = 1; 36 37 /// Set the number of columns present in a grid. 38 void apply(GridFrame grid) { 39 40 grid.segmentCount = amount; 41 42 } 43 44 /// Set the number of columns used by this node. 45 void apply(Node node) { 46 47 node.layout.expand = amount; 48 49 } 50 51 } 52 53 /// ditto 54 Segments segments(uint columns) { 55 56 return Segments(columns); 57 58 } 59 60 /// ditto 61 Segments segments(uint columns)() { 62 63 return Segments(columns); 64 65 } 66 67 /// The GridFrame node will align its children in a 2D grid. 68 class GridFrame : Frame { 69 70 size_t segmentCount; 71 72 private { 73 74 /// Sizes for each segment. 75 int[] segmentSizes; 76 77 /// Last grid width given. 78 float lastWidth; 79 80 } 81 82 this(Ts...)(Ts children) { 83 84 this.children.length = children.length; 85 86 // Check the other arguments 87 static foreach (i, arg; children) {{ 88 89 // Grid row (via array) 90 static if (is(typeof(arg) : U[], U)) { 91 92 this.children[i] = gridRow(arg); 93 94 } 95 96 // Other stuff 97 else { 98 99 this.children[i] = arg; 100 101 } 102 103 }} 104 105 } 106 107 unittest { 108 109 import std.math; 110 import std.array; 111 import std.typecons; 112 import fluid.label; 113 114 auto io = new HeadlessBackend; 115 auto root = gridFrame( 116 .nullTheme, 117 .layout!"fill", 118 .segments!4, 119 120 label("You can make tables and grids with Grid"), 121 [ 122 label("This"), 123 label("Is"), 124 label("A"), 125 label("Grid"), 126 ], 127 [ 128 label(.segments!2, "Multiple columns"), 129 label(.segments!2, "For a single cell"), 130 ] 131 ); 132 133 root.io = io; 134 root.draw(); 135 136 // Check layout parameters 137 138 assert(root.layout == .layout!"fill"); 139 assert(root.segmentCount == 4); 140 assert(root.children.length == 3); 141 142 assert(cast(Label) root.children[0]); 143 144 auto row1 = cast(GridRow) root.children[1]; 145 146 assert(row1); 147 assert(row1.segmentCount == 4); 148 assert(row1.children.all!"a.layout.expand == 0"); 149 150 auto row2 = cast(GridRow) root.children[2]; 151 152 assert(row2); 153 assert(row2.segmentCount == 4); 154 assert(row2.children.all!"a.layout.expand == 2"); 155 156 // Current implementation requires an extra frame to settle. This shouldn't be necessary. 157 io.nextFrame; 158 root.draw(); 159 160 // Each column should be 200px wide 161 assert(root.segmentSizes == [200, 200, 200, 200]); 162 163 const rowEnds = root.children.map!(a => a.minSize.y) 164 .cumulativeFold!"a + b" 165 .array; 166 167 // Check if the drawing is correct 168 // Row 0 169 io.assertTexture(Rectangle(0, 0, root.children[0].minSize.tupleof), color!"fff"); 170 171 // Row 1 172 foreach (i; 0..4) { 173 174 const start = Vector2(i * 200, rowEnds[0]); 175 176 assert(io.textures.canFind!(tex => tex.isStartClose(start))); 177 178 } 179 180 // Row 2 181 foreach (i; 0..2) { 182 183 const start = Vector2(i * 400, rowEnds[1]); 184 185 assert(io.textures.canFind!(tex => tex.isStartClose(start))); 186 187 } 188 189 } 190 191 /// Add a new row to this grid. 192 void addRow(Ts...)(Ts content) { 193 194 children ~= gridRow(content); 195 196 } 197 198 /// Magic to extract return value of extractParams at compile time. 199 private struct Number(size_t num) { 200 201 enum value = num; 202 203 } 204 205 /// Evaluate special parameters and get the index of the first non-special parameter (not Segments, Layout nor 206 /// Theme). 207 /// Returns: An instance of `Number` with said index as parameter. 208 private auto extractParams(Args...)(Args args) { 209 210 enum maxInitialArgs = min(args.length, 3); 211 212 static foreach (i, arg; args[0..maxInitialArgs]) { 213 214 // Complete; wait to the end 215 static if (__traits(compiles, endIndex)) { } 216 217 // Segment count 218 else static if (is(typeof(arg) : Segments)) { 219 220 segmentCount = arg.expand; 221 222 } 223 224 // Layout 225 else static if (is(typeof(arg) : Layout)) { 226 227 layout = arg; 228 229 } 230 231 // Theme 232 else static if (is(typeof(arg) : Theme)) { 233 234 theme = arg; 235 236 } 237 238 // Mark this as the end 239 else enum endIndex = i; 240 241 } 242 243 static if (!__traits(compiles, endIndex)) { 244 245 enum endIndex = maxInitialArgs; 246 247 } 248 249 return Number!endIndex(); 250 251 } 252 253 override protected void resizeImpl(Vector2 space) { 254 255 import std.numeric; 256 257 // Need to recalculate segments 258 if (segmentCount == 0) { 259 260 // Increase segment count 261 segmentCount = 1; 262 263 // Check children 264 foreach (child; children) { 265 266 // Only count rows 267 if (auto row = cast(GridRow) child) { 268 269 // Set self as parent 270 row.parent = this; 271 272 // Recalculate the segments needed by the row 273 row.calculateSegments(); 274 275 // Set the segment count to the lowest common multiple of the current segment count and the cell 276 // count of this row 277 segmentCount = lcm(segmentCount, row.segmentCount); 278 279 } 280 281 } 282 283 } 284 285 else { 286 287 foreach (child; children) { 288 289 // Assign self as parent to all rows 290 if (auto row = cast(GridRow) child) { 291 row.parent = this; 292 } 293 294 } 295 296 } 297 298 // Reserve the segments 299 segmentSizes = new int[segmentCount]; 300 301 // Resize the children 302 super.resizeImpl(space); 303 304 // Reset width 305 lastWidth = 0; 306 307 } 308 309 override void drawImpl(Rectangle outer, Rectangle inner) { 310 311 // TODO WHY is this done here and not in resizeImpl? 312 void expand(Node child) { 313 314 // Given more grid space than we allocated 315 if (lastWidth >= inner.width + 1) return; 316 317 // Only proceed if the given node is a row 318 if (!cast(GridRow) child) return; 319 320 // Update the width 321 lastWidth = inner.width; 322 323 // Get margin for the row 324 const rowMargin = child.style.totalMargin; 325 // Note: We're assuming all rows have the same margin. This might not hold true with the introduction of 326 // tags. 327 328 // Expand the segments to match box size 329 redistributeSpace(segmentSizes, inner.width - rowMargin.sideLeft - rowMargin.sideRight); 330 331 } 332 333 // Draw the background 334 pickStyle.drawBackground(tree.io, outer); 335 336 // Get the position 337 auto position = inner.y; 338 339 // Draw the rows 340 foreach (child; filterChildren) { 341 342 // Get params 343 const rect = Rectangle( 344 inner.x, position, 345 inner.width, child.minSize.y 346 ); 347 348 // Try to expand grid segments 349 expand(child); 350 351 // Draw the child 352 child.draw(rect); 353 354 // Offset position 355 position += child.minSize.y + style.gap.sideY; 356 357 } 358 359 } 360 361 unittest { 362 363 import fluid.label; 364 365 // Nodes are to span segments in order: 366 // 1. One label to span 6 segments 367 // 2. Each 3 segments 368 // 3. Each 2 segments 369 auto g = gridFrame( 370 [ label("") ], 371 [ label(""), label("") ], 372 [ label(""), label(""), label("") ], 373 ); 374 375 g.backend = new HeadlessBackend; 376 g.draw(); 377 378 assert(g.segmentCount == 6); 379 380 } 381 382 } 383 384 /// A single row in a `Grid`. 385 class GridRow : Frame { 386 387 GridFrame parent; 388 size_t segmentCount; 389 390 /// Params: 391 /// nodes = Children to be placed in the row. 392 this(Ts...)(Ts nodes) { 393 394 super(nodes); 395 this.layout.nodeAlign = NodeAlign.fill; 396 this.directionHorizontal = true; 397 398 } 399 400 void calculateSegments() { 401 402 segmentCount = 0; 403 404 // Count segments used by each child 405 foreach (child; children) { 406 407 segmentCount += either(child.layout.expand, 1); 408 409 } 410 411 } 412 413 override void resizeImpl(Vector2 space) { 414 415 // Reset the size 416 minSize = Vector2(); 417 418 // Empty row; do nothing 419 if (children.length == 0) return; 420 421 // No segments calculated, run now 422 if (segmentCount == 0) { 423 424 calculateSegments(); 425 426 } 427 428 size_t segment; 429 430 // Resize the children 431 foreach (i, child; children) { 432 433 const segments = either(child.layout.expand, 1); 434 const gapSpace = max( 435 0, 436 style.gap.sideX * (cast(ptrdiff_t) children.length - 1) 437 ); 438 const childSpace = Vector2( 439 space.x * segments / segmentCount - gapSpace, 440 minSize.y, 441 ); 442 443 scope (exit) segment += segments; 444 445 // Resize the child 446 child.resize(tree, theme, childSpace); 447 448 auto range = parent.segmentSizes[segment..segment+segments]; 449 450 // Include the gap for all, but the first child 451 const gap = i == 0 ? 0 : style.gap.sideX; 452 453 // Second step: Expand the segments to give some space for the child 454 minSize.x += range.redistributeSpace(child.minSize.x + gap); 455 456 // Increase vertical space, if needed 457 if (child.minSize.y > minSize.y) { 458 459 minSize.y = child.minSize.y; 460 461 } 462 463 } 464 465 } 466 467 override protected void drawImpl(Rectangle outer, Rectangle inner) { 468 469 size_t segment; 470 471 pickStyle.drawBackground(tree.io, outer); 472 473 /// Child position. 474 auto position = Vector2(inner.x, inner.y); 475 476 foreach (i, child; filterChildren) { 477 478 const segments = either(child.layout.expand, 1); 479 const gap = i == 0 ? 0 : style.gap.sideX; 480 const width = parent.segmentSizes[segment..segment+segments].sum; 481 482 // Draw the child 483 child.draw(Rectangle( 484 position.x + gap, position.y, 485 width - gap, inner.height, 486 )); 487 488 // Proceed to the next segment 489 segment += segments; 490 position.x += width; 491 492 } 493 494 } 495 496 @("Grid rows can have gaps") 497 unittest { 498 499 auto theme = nullTheme.derive( 500 rule!GridFrame( 501 Rule.gap = 4, 502 ), 503 rule!GridRow( 504 Rule.gap = 6, 505 ), 506 ); 507 508 static class Warden : Frame { 509 510 Vector2 position; 511 512 override void resizeImpl(Vector2 space) { 513 super.resizeImpl(space); 514 minSize = Vector2(10, 10); 515 } 516 517 override void drawImpl(Rectangle outer, Rectangle) { 518 position = outer.start; 519 } 520 521 } 522 523 alias warden = simpleConstructor!Warden; 524 525 Warden[3] row1; 526 Warden[6] row2; 527 528 auto grid = gridFrame( 529 theme, 530 [ 531 row1[0] = warden(.segments!2), 532 row1[1] = warden(.segments!2), 533 row1[2] = warden(.segments!2), 534 ], 535 [ 536 row2[0] = warden(), 537 row2[1] = warden(), 538 row2[2] = warden(), 539 row2[3] = warden(), 540 row2[4] = warden(), 541 row2[5] = warden(), 542 ], 543 ); 544 545 grid.draw(); 546 547 assert(row1[0].position == Vector2( 0, 0)); 548 assert(row1[1].position == Vector2(32, 0)); 549 assert(row1[2].position == Vector2(64, 0)); 550 551 assert(row2[0].position == Vector2( 0, 14)); 552 assert(row2[1].position == Vector2(16, 14)); 553 assert(row2[2].position == Vector2(32, 14)); 554 assert(row2[3].position == Vector2(48, 14)); 555 assert(row2[4].position == Vector2(64, 14)); 556 assert(row2[5].position == Vector2(80, 14)); 557 558 } 559 560 } 561 562 /// Redistribute space for the given row spacing range. It will increase the size of as many cells as possible as long 563 /// as they can stay even. 564 /// 565 /// Does nothing if amount of space was reduced. 566 /// 567 /// Params: 568 /// range = Range to work on and modify. 569 /// space = New amount of space to apply. The resulting sum of range items will be equal or greater (if it was 570 /// already greater) to this number. 571 /// Returns: 572 /// Newly acquired amount of space, the resulting sum of range size. 573 private ElementType!Range redistributeSpace(Range, Numeric)(ref Range range, Numeric space, string caller = __FUNCTION__) { 574 575 import std.math; 576 577 alias RangeNumeric = ElementType!Range; 578 579 // Get a sorted copy of the range 580 auto sortedCopy = range.dup.sort!"a > b"; 581 582 // Find smallest item & current size of the range 583 RangeNumeric currentSize; 584 RangeNumeric smallestItem; 585 586 // Check current data from the range 587 foreach (item; sortedCopy.save) { 588 589 currentSize += item; 590 smallestItem = item; 591 592 } 593 594 /// Extra space to give 595 auto extra = cast(double) space - currentSize; 596 597 // Do nothing if there's no extra space 598 if (extra < 0 || extra.isClose(0)) return currentSize; 599 600 /// Space to give per segment 601 RangeNumeric perElement; 602 603 // Check all segments 604 foreach (i, segment; sortedCopy.enumerate) { 605 606 // Split the available over all remaining segments 607 perElement = smallestItem + cast(RangeNumeric) ceil(extra / (range.length - i)); 608 609 // Skip segments if the resulting size isn't big enough 610 if (perElement > segment) break; 611 612 } 613 614 RangeNumeric total; 615 616 // Assign the size 617 foreach (ref item; range) { 618 619 // Grow this one 620 item = max(item, perElement); 621 total += item; 622 623 } 624 625 return total; 626 627 }