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