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 @safe: 15 16 17 deprecated("`Grid` and `grid` were renamed to `GridFrame` and `gridFrame` respectively. To be removed in 0.8.0.") { 18 19 alias grid = simpleConstructor!GridFrame; 20 alias Grid = GridFrame; 21 22 } 23 24 alias gridFrame = simpleConstructor!GridFrame; 25 alias gridRow = simpleConstructor!GridRow; 26 27 // TODO rename segments to columns? 28 29 /// Segments is used to set the number of columns spanned by a grid item. When applied to a grid, it sets the number of 30 /// columns the grid will have. 31 struct Segments { 32 33 /// Number of columns used by a grid item. 34 uint amount = 1; 35 36 /// Set the number of columns present in a grid. 37 void apply(GridFrame grid) { 38 39 grid.segmentCount = amount; 40 41 } 42 43 /// Set the number of columns used by this node. 44 void apply(Node node) { 45 46 node.layout.expand = amount; 47 48 } 49 50 } 51 52 /// ditto 53 Segments segments(uint columns) { 54 55 return Segments(columns); 56 57 } 58 59 /// ditto 60 Segments segments(uint columns)() { 61 62 return Segments(columns); 63 64 } 65 66 /// The GridFrame node will align its children in a 2D grid. 67 class GridFrame : Frame { 68 69 size_t segmentCount; 70 71 private { 72 73 /// Sizes for each segment. 74 int[] _segmentSizes; 75 76 /// Last grid width given. 77 float lastWidth; 78 79 } 80 81 this(Ts...)(Ts children) { 82 83 this.children.length = children.length; 84 85 // Check the other arguments 86 static foreach (i, arg; children) {{ 87 88 // Grid row (via array) 89 static if (is(typeof(arg) : U[], U)) { 90 91 this.children[i] = gridRow(arg); 92 93 } 94 95 // Other stuff 96 else { 97 98 this.children[i] = arg; 99 100 } 101 102 }} 103 104 } 105 106 /// Add a new row to this grid. 107 void addRow(Ts...)(Ts content) { 108 109 children ~= gridRow(content); 110 111 } 112 113 /// Magic to extract return value of extractParams at compile time. 114 private struct Number(size_t num) { 115 116 enum value = num; 117 118 } 119 120 /// Returns: 121 /// An array of numbers indicating the width of each segment in the grid. 122 const(int)[] segmentSizes() const { 123 124 return _segmentSizes; 125 126 } 127 128 /// Evaluate special parameters and get the index of the first non-special parameter (not Segments, Layout nor 129 /// Theme). 130 /// Returns: An instance of `Number` with said index as parameter. 131 private auto extractParams(Args...)(Args args) { 132 133 enum maxInitialArgs = min(args.length, 3); 134 135 static foreach (i, arg; args[0..maxInitialArgs]) { 136 137 // Complete; wait to the end 138 static if (__traits(compiles, endIndex)) { } 139 140 // Segment count 141 else static if (is(typeof(arg) : Segments)) { 142 143 segmentCount = arg.expand; 144 145 } 146 147 // Layout 148 else static if (is(typeof(arg) : Layout)) { 149 150 layout = arg; 151 152 } 153 154 // Theme 155 else static if (is(typeof(arg) : Theme)) { 156 157 theme = arg; 158 159 } 160 161 // Mark this as the end 162 else enum endIndex = i; 163 164 } 165 166 static if (!__traits(compiles, endIndex)) { 167 168 enum endIndex = maxInitialArgs; 169 170 } 171 172 return Number!endIndex(); 173 174 } 175 176 override protected void resizeImpl(Vector2 space) { 177 178 import std.numeric; 179 180 // Need to recalculate segments 181 if (segmentCount == 0) { 182 183 // Increase segment count 184 segmentCount = 1; 185 186 // Check children 187 foreach (child; children) { 188 189 // Only count rows 190 if (auto row = cast(GridRow) child) { 191 192 // Set self as parent 193 row.parent = this; 194 195 // Recalculate the segments needed by the row 196 row.calculateSegments(); 197 198 // Set the segment count to the lowest common multiple of the current segment count and the cell 199 // count of this row 200 segmentCount = lcm(segmentCount, row.segmentCount); 201 202 } 203 204 } 205 206 } 207 208 else { 209 210 foreach (child; children) { 211 212 // Assign self as parent to all rows 213 if (auto row = cast(GridRow) child) { 214 row.parent = this; 215 } 216 217 } 218 219 } 220 221 // Reserve the segments 222 _segmentSizes = new int[segmentCount]; 223 224 // Resize the children 225 super.resizeImpl(space); 226 227 // Reset width 228 lastWidth = 0; 229 230 } 231 232 override void drawImpl(Rectangle outer, Rectangle inner) { 233 234 // TODO WHY is this done here and not in resizeImpl? 235 void expand(Node child) { 236 237 // Given more grid space than we allocated 238 if (lastWidth >= inner.width + 1) return; 239 240 // Only proceed if the given node is a row 241 if (!cast(GridRow) child) return; 242 243 // Update the width 244 lastWidth = inner.width; 245 246 // Get margin for the row 247 const rowMargin = child.style.totalMargin; 248 // Note: We're assuming all rows have the same margin. This might not hold true with the introduction of 249 // tags. 250 251 // Expand the segments to match box size 252 redistributeSpace(_segmentSizes, inner.width - rowMargin.sideLeft - rowMargin.sideRight); 253 254 } 255 256 // Draw the background 257 pickStyle.drawBackground(tree.io, canvasIO, outer); 258 259 // Get the position 260 auto position = inner.y; 261 262 // Draw the rows 263 foreach (child; filterChildren) { 264 265 // Get params 266 const rect = Rectangle( 267 inner.x, position, 268 inner.width, child.minSize.y 269 ); 270 271 // Try to expand grid segments 272 expand(child); 273 274 // Draw the child 275 drawChild(child, rect); 276 277 // Offset position 278 position += child.minSize.y + style.gap.sideY; 279 280 } 281 282 } 283 284 } 285 286 /// A single row in a `Grid`. 287 class GridRow : Frame { 288 289 GridFrame parent; 290 size_t segmentCount; 291 292 /// Params: 293 /// nodes = Children to be placed in the row. 294 this(Ts...)(Ts nodes) { 295 296 super(nodes); 297 this.layout.nodeAlign = NodeAlign.fill; 298 this.directionHorizontal = true; 299 300 } 301 302 void calculateSegments() { 303 304 segmentCount = 0; 305 306 // Count segments used by each child 307 foreach (child; children) { 308 309 segmentCount += either(child.layout.expand, 1); 310 311 } 312 313 } 314 315 override void resizeImpl(Vector2 space) { 316 317 use(canvasIO); 318 319 // Reset the size 320 minSize = Vector2(); 321 322 // Empty row; do nothing 323 if (children.length == 0) return; 324 325 // No segments calculated, run now 326 if (segmentCount == 0) { 327 328 calculateSegments(); 329 330 } 331 332 size_t segment; 333 334 // Resize the children 335 foreach (i, child; children) { 336 337 const segments = either(child.layout.expand, 1); 338 const gapSpace = max( 339 0, 340 style.gap.sideX * (cast(ptrdiff_t) children.length - 1) 341 ); 342 const childSpace = Vector2( 343 space.x * segments / segmentCount - gapSpace, 344 minSize.y, 345 ); 346 347 scope (exit) segment += segments; 348 349 // Resize the child 350 resizeChild(child, childSpace); 351 352 auto range = parent._segmentSizes[segment..segment+segments]; 353 354 // Include the gap for all, but the first child 355 const gap = i == 0 ? 0 : style.gap.sideX; 356 357 // Second step: Expand the segments to give some space for the child 358 minSize.x += range.redistributeSpace(child.minSize.x + gap); 359 360 // Increase vertical space, if needed 361 if (child.minSize.y > minSize.y) { 362 363 minSize.y = child.minSize.y; 364 365 } 366 367 } 368 369 } 370 371 override protected void drawImpl(Rectangle outer, Rectangle inner) { 372 373 size_t segment; 374 375 pickStyle.drawBackground(tree.io, canvasIO, outer); 376 377 /// Child position. 378 auto position = Vector2(inner.x, inner.y); 379 380 foreach (i, child; filterChildren) { 381 382 const segments = either(child.layout.expand, 1); 383 const gap = i == 0 ? 0 : style.gap.sideX; 384 const width = parent.segmentSizes[segment..segment+segments].sum; 385 386 // Draw the child 387 drawChild(child, Rectangle( 388 position.x + gap, position.y, 389 width - gap, inner.height, 390 )); 391 392 // Proceed to the next segment 393 segment += segments; 394 position.x += width; 395 396 } 397 398 } 399 400 } 401 402 /// Redistribute space for the given row spacing range. It will increase the size of as many cells as possible as long 403 /// as they can stay even. 404 /// 405 /// Does nothing if amount of space was reduced. 406 /// 407 /// Params: 408 /// range = Range to work on and modify. 409 /// space = New amount of space to apply. The resulting sum of range items will be equal or greater (if it was 410 /// already greater) to this number. 411 /// Returns: 412 /// Newly acquired amount of space, the resulting sum of range size. 413 private ElementType!Range redistributeSpace(Range, Numeric)(ref Range range, Numeric space, string caller = __FUNCTION__) { 414 415 import std.math; 416 417 alias RangeNumeric = ElementType!Range; 418 419 // Get a sorted copy of the range 420 auto sortedCopy = range.dup.sort!"a > b"; 421 422 // Find smallest item & current size of the range 423 RangeNumeric currentSize; 424 RangeNumeric smallestItem; 425 426 // Check current data from the range 427 foreach (item; sortedCopy.save) { 428 429 currentSize += item; 430 smallestItem = item; 431 432 } 433 434 /// Extra space to give 435 auto extra = cast(double) space - currentSize; 436 437 // Do nothing if there's no extra space 438 if (extra < 0 || extra.isClose(0)) return currentSize; 439 440 /// Space to give per segment 441 RangeNumeric perElement; 442 443 // Check all segments 444 foreach (i, segment; sortedCopy.enumerate) { 445 446 // Split the available over all remaining segments 447 perElement = smallestItem + cast(RangeNumeric) ceil(extra / (range.length - i)); 448 449 // Skip segments if the resulting size isn't big enough 450 if (perElement > segment) break; 451 452 } 453 454 RangeNumeric total; 455 456 // Assign the size 457 foreach (ref item; range) { 458 459 // Grow this one 460 item = max(item, perElement); 461 total += item; 462 463 } 464 465 return total; 466 467 }