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("Renamed to `gridFrame`") alias grid = simpleConstructor!Grid; 19 alias gridFrame = grid; 20 alias gridRow = simpleConstructor!GridRow; 21 22 // TODO rename segments to columns? 23 24 /// Segments is used to set the number of columns spanned by a grid item. When applied to a grid, it sets the number of 25 /// columns the grid will have. 26 struct Segments { 27 28 /// Number of columns used by a grid item. 29 uint amount = 1; 30 31 /// Set the number of columns present in a grid. 32 void apply(Grid grid) { 33 34 grid.segmentCount = amount; 35 36 } 37 38 /// Set the number of columns used by this node. 39 void apply(Node node) { 40 41 node.layout.expand = amount; 42 43 } 44 45 } 46 47 /// ditto 48 Segments segments(uint columns) { 49 50 return Segments(columns); 51 52 } 53 54 /// ditto 55 Segments segments(uint columns)() { 56 57 return Segments(columns); 58 59 } 60 61 deprecated("Renamed to `GridFrame`") alias Grid = GridFrame; 62 63 /// The GridFrame node will align its children in a 2D grid. 64 class GridFrame : Frame { 65 66 size_t segmentCount; 67 68 private { 69 70 /// Sizes for each segment. 71 int[] segmentSizes; 72 73 /// Last grid width given. 74 float lastWidth; 75 76 } 77 78 this(Ts...)(Ts children) { 79 80 this.children.length = children.length; 81 82 // Check the other arguments 83 static foreach (i, arg; children) {{ 84 85 // Grid row (via array) 86 static if (is(typeof(arg) : U[], U)) { 87 88 this.children[i] = gridRow(this, arg); 89 90 } 91 92 // Other stuff 93 else { 94 95 this.children[i] = arg; 96 97 } 98 99 }} 100 101 } 102 103 unittest { 104 105 import std.math; 106 import std.array; 107 import std.typecons; 108 import fluid.label; 109 110 auto io = new HeadlessBackend; 111 auto root = grid( 112 .Theme.init.makeTheme!q{ 113 Label.styleAdd!q{ 114 textColor = color!"000"; 115 }; 116 }, 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(this, 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 // Recalculate the segments needed by the row 270 row.calculateSegments(); 271 272 // Set the segment count to the lowest common multiple of the current segment count and the cell 273 // count of this row 274 segmentCount = lcm(segmentCount, row.segmentCount); 275 276 } 277 278 } 279 280 } 281 282 // Reserve the segments 283 segmentSizes = new int[segmentCount]; 284 285 // Resize the children 286 super.resizeImpl(space); 287 288 // Reset width 289 lastWidth = 0; 290 291 } 292 293 override void drawImpl(Rectangle outer, Rectangle inner) { 294 295 // TODO WHY is this done here and not in resizeImpl? 296 void expand(Node child) { 297 298 // Given more grid space than we allocated 299 if (lastWidth >= inner.width + 1) return; 300 301 // Only proceed if the given node is a row 302 if (!cast(GridRow) child) return; 303 304 // Update the width 305 lastWidth = inner.width; 306 307 // Get margin for the row 308 const rowMargin = child.style.totalMargin; 309 // Note: We're assuming all rows have the same margin. This might not hold true with the introduction of 310 // tags. 311 312 // Expand the segments to match box size 313 redistributeSpace(segmentSizes, inner.width - rowMargin.sideLeft - rowMargin.sideRight); 314 315 } 316 317 // Draw the background 318 pickStyle.drawBackground(tree.io, outer); 319 320 // Get the position 321 auto position = inner.y; 322 323 // Draw the rows 324 foreach (child; filterChildren) { 325 326 // Get params 327 const rect = Rectangle( 328 inner.x, position, 329 inner.width, child.minSize.y 330 ); 331 332 // Try to expand grid segments 333 expand(child); 334 335 // Draw the child 336 child.draw(rect); 337 338 // Offset position 339 position += child.minSize.y + style.gap; 340 341 } 342 343 } 344 345 unittest { 346 347 import fluid.label; 348 349 // Nodes are to span segments in order: 350 // 1. One label to span 6 segments 351 // 2. Each 3 segments 352 // 3. Each 2 segments 353 auto g = grid( 354 [ label("") ], 355 [ label(""), label("") ], 356 [ label(""), label(""), label("") ], 357 ); 358 359 g.backend = new HeadlessBackend; 360 g.draw(); 361 362 assert(g.segmentCount == 6); 363 364 } 365 366 } 367 368 /// A single row in a `Grid`. 369 class GridRow : Frame { 370 371 Grid parent; 372 size_t segmentCount; 373 374 /// Params: 375 /// params = Standard Fluid constructor parameters. 376 /// parent = Grid this row will be placed in. 377 /// nodes = Children to be placed in the row. 378 this(T...)(Grid parent, T nodes) { 379 380 super(nodes); 381 this.layout.nodeAlign = NodeAlign.fill; 382 this.parent = parent; 383 this.directionHorizontal = true; 384 385 } 386 387 void calculateSegments() { 388 389 segmentCount = 0; 390 391 // Count segments used by each child 392 foreach (child; children) { 393 394 segmentCount += either(child.layout.expand, 1); 395 396 } 397 398 } 399 400 override void resizeImpl(Vector2 space) { 401 402 // Reset the size 403 minSize = Vector2(); 404 405 // Empty row; do nothing 406 if (children.length == 0) return; 407 408 // No segments calculated, run now 409 if (segmentCount == 0) { 410 411 calculateSegments(); 412 413 } 414 415 size_t segment; 416 417 // Resize the children 418 foreach (child; children) { 419 420 const segments = either(child.layout.expand, 1); 421 const childSpace = Vector2( 422 space.x * segments / segmentCount, 423 minSize.y, 424 ); 425 426 scope (exit) segment += segments; 427 428 // Resize the child 429 child.resize(tree, theme, childSpace); 430 431 auto range = parent.segmentSizes[segment..segment+segments]; 432 433 // Second step: Expand the segments to give some space for the child 434 minSize.x += range.redistributeSpace(child.minSize.x); 435 436 // Increase vertical space, if needed 437 if (child.minSize.y > minSize.y) { 438 439 minSize.y = child.minSize.y; 440 441 } 442 443 } 444 445 } 446 447 override protected void drawImpl(Rectangle outer, Rectangle inner) { 448 449 size_t segment; 450 451 pickStyle.drawBackground(tree.io, outer); 452 453 /// Child position. 454 auto position = Vector2(inner.x, inner.y); 455 456 foreach (child; filterChildren) { 457 458 const segments = either(child.layout.expand, 1); 459 const width = parent.segmentSizes[segment..segment+segments].sum; 460 461 // Draw the child 462 child.draw(Rectangle( 463 position.x, position.y, 464 width, inner.height, 465 )); 466 467 // Proceed to the next segment 468 segment += segments; 469 position.x += width; 470 471 } 472 473 } 474 475 } 476 477 /// Redistribute space for the given row spacing range. It will increase the size of as many cells as possible as long 478 /// as they can stay even. 479 /// 480 /// Does nothing if amount of space was reduced. 481 /// 482 /// Params: 483 /// range = Range to work on and modify. 484 /// space = New amount of space to apply. The resulting sum of range items will be equal or greater (if it was 485 /// already greater) to this number. 486 /// Returns: 487 /// Newly acquired amount of space, the resulting sum of range size. 488 private ElementType!Range redistributeSpace(Range, Numeric)(ref Range range, Numeric space, string caller = __FUNCTION__) { 489 490 import std.math; 491 492 alias RangeNumeric = ElementType!Range; 493 494 // Get a sorted copy of the range 495 auto sortedCopy = range.dup.sort!"a > b"; 496 497 // Find smallest item & current size of the range 498 RangeNumeric currentSize; 499 RangeNumeric smallestItem; 500 501 // Check current data from the range 502 foreach (item; sortedCopy.save) { 503 504 currentSize += item; 505 smallestItem = item; 506 507 } 508 509 /// Extra space to give 510 auto extra = cast(double) space - currentSize; 511 512 // Do nothing if there's no extra space 513 if (extra < 0 || extra.isClose(0)) return currentSize; 514 515 /// Space to give per segment 516 RangeNumeric perElement; 517 518 // Check all segments 519 foreach (i, segment; sortedCopy.enumerate) { 520 521 // Split the available over all remaining segments 522 perElement = smallestItem + cast(RangeNumeric) ceil(extra / (range.length - i)); 523 524 // Skip segments if the resulting size isn't big enough 525 if (perElement > segment) break; 526 527 } 528 529 RangeNumeric total; 530 531 // Assign the size 532 foreach (ref item; range) { 533 534 // Grow this one 535 item = max(item, perElement); 536 total += item; 537 538 } 539 540 return total; 541 542 }