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