A game framework written with osu! in mind.
at master 450 lines 17 kB view raw
1// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. 2// See the LICENCE file in the repository root for full licence text. 3 4using System; 5using System.Collections.Generic; 6using System.Linq; 7using osu.Framework.Allocation; 8using osu.Framework.Caching; 9using osu.Framework.Extensions.EnumExtensions; 10using osu.Framework.Layout; 11using osuTK; 12 13namespace osu.Framework.Graphics.Containers 14{ 15 /// <summary> 16 /// A container which allows laying out <see cref="Drawable"/>s in a grid. 17 /// </summary> 18 public class GridContainer : CompositeDrawable 19 { 20 public GridContainer() 21 { 22 AddLayout(cellLayout); 23 AddLayout(cellChildLayout); 24 } 25 26 [BackgroundDependencyLoader] 27 private void load() 28 { 29 layoutContent(); 30 } 31 32 private GridContainerContent content; 33 34 /// <summary> 35 /// The content of this <see cref="GridContainer"/>, arranged in a 2D grid array, where each array 36 /// of <see cref="Drawable"/>s represents a row and each element of that array represents a column. 37 /// <para> 38 /// Null elements are allowed to represent blank rows/cells. 39 /// </para> 40 /// </summary> 41 public GridContainerContent Content 42 { 43 get => content; 44 set 45 { 46 if (content?.Equals(value) == true) 47 return; 48 49 if (content != null) 50 content.ArrayElementChanged -= onContentChange; 51 52 content = value; 53 54 onContentChange(); 55 56 if (content != null) 57 content.ArrayElementChanged += onContentChange; 58 } 59 } 60 61 private void onContentChange() 62 { 63 cellContent.Invalidate(); 64 } 65 66 private Dimension[] rowDimensions = Array.Empty<Dimension>(); 67 68 /// <summary> 69 /// Explicit dimensions for rows. Each index of this array applies to the respective row index inside <see cref="Content"/>. 70 /// </summary> 71 public Dimension[] RowDimensions 72 { 73 set 74 { 75 if (value == null) 76 throw new ArgumentNullException(nameof(value)); 77 78 if (rowDimensions == value) 79 return; 80 81 rowDimensions = value; 82 83 cellLayout.Invalidate(); 84 } 85 } 86 87 private Dimension[] columnDimensions = Array.Empty<Dimension>(); 88 89 /// <summary> 90 /// Explicit dimensions for columns. Each index of this array applies to the respective column index inside <see cref="Content"/>. 91 /// </summary> 92 public Dimension[] ColumnDimensions 93 { 94 set 95 { 96 if (value == null) 97 throw new ArgumentNullException(nameof(value)); 98 99 if (columnDimensions == value) 100 return; 101 102 columnDimensions = value; 103 104 cellLayout.Invalidate(); 105 } 106 } 107 108 /// <summary> 109 /// Controls which <see cref="Axes"/> are automatically sized w.r.t. <see cref="CompositeDrawable.InternalChildren"/>. 110 /// Children's <see cref="Drawable.BypassAutoSizeAxes"/> are ignored for automatic sizing. 111 /// Most notably, <see cref="Drawable.RelativePositionAxes"/> and <see cref="Drawable.RelativeSizeAxes"/> of children 112 /// do not affect automatic sizing to avoid circular size dependencies. 113 /// It is not allowed to manually set <see cref="Drawable.Size"/> (or <see cref="Drawable.Width"/> / <see cref="Drawable.Height"/>) 114 /// on any <see cref="Axes"/> which are automatically sized. 115 /// </summary> 116 public new Axes AutoSizeAxes 117 { 118 get => base.AutoSizeAxes; 119 set => base.AutoSizeAxes = value; 120 } 121 122 protected override void Update() 123 { 124 base.Update(); 125 126 layoutContent(); 127 layoutCells(); 128 } 129 130 private readonly Cached cellContent = new Cached(); 131 private readonly LayoutValue cellLayout = new LayoutValue(Invalidation.DrawInfo | Invalidation.RequiredParentSizeToFit); 132 private readonly LayoutValue cellChildLayout = new LayoutValue(Invalidation.RequiredParentSizeToFit | Invalidation.Presence, InvalidationSource.Child); 133 134 private CellContainer[,] cells = new CellContainer[0, 0]; 135 private int cellRows => cells.GetLength(0); 136 private int cellColumns => cells.GetLength(1); 137 138 /// <summary> 139 /// Moves content from <see cref="Content"/> into cells. 140 /// </summary> 141 private void layoutContent() 142 { 143 if (cellContent.IsValid) 144 return; 145 146 int requiredRows = Content?.Count ?? 0; 147 int requiredColumns = requiredRows == 0 ? 0 : Content?.Max(c => c?.Count ?? 0) ?? 0; 148 149 // Clear cell containers without disposing, as the content might be reused 150 foreach (var cell in cells) 151 cell.Clear(false); 152 153 // It's easier to just re-construct the cell containers instead of resizing 154 // If this becomes a bottleneck we can transition to using lists, but this keeps the structure clean... 155 ClearInternal(); 156 cellLayout.Invalidate(); 157 158 // Create the new cell containers and add content 159 cells = new CellContainer[requiredRows, requiredColumns]; 160 161 for (int r = 0; r < cellRows; r++) 162 { 163 for (int c = 0; c < cellColumns; c++) 164 { 165 // Add cell 166 cells[r, c] = new CellContainer(); 167 168 // Allow empty rows 169 if (Content[r] == null) 170 continue; 171 172 // Allow non-square grids 173 if (c >= Content[r].Count) 174 continue; 175 176 // Allow empty cells 177 if (Content[r][c] == null) 178 continue; 179 180 // Add content 181 cells[r, c].Add(Content[r][c]); 182 cells[r, c].Depth = Content[r][c].Depth; 183 184 AddInternal(cells[r, c]); 185 } 186 } 187 188 cellContent.Validate(); 189 } 190 191 /// <summary> 192 /// Repositions/resizes cells. 193 /// </summary> 194 private void layoutCells() 195 { 196 if (!cellChildLayout.IsValid) 197 { 198 cellLayout.Invalidate(); 199 cellChildLayout.Validate(); 200 } 201 202 if (cellLayout.IsValid) 203 return; 204 205 var widths = distribute(columnDimensions, DrawWidth, getCellSizesAlongAxis(Axes.X, DrawWidth)); 206 var heights = distribute(rowDimensions, DrawHeight, getCellSizesAlongAxis(Axes.Y, DrawHeight)); 207 208 for (int col = 0; col < cellColumns; col++) 209 { 210 for (int row = 0; row < cellRows; row++) 211 { 212 cells[row, col].Size = new Vector2(widths[col], heights[row]); 213 214 if (col > 0) 215 cells[row, col].X = cells[row, col - 1].X + cells[row, col - 1].Width; 216 217 if (row > 0) 218 cells[row, col].Y = cells[row - 1, col].Y + cells[row - 1, col].Height; 219 } 220 } 221 222 cellLayout.Validate(); 223 } 224 225 /// <summary> 226 /// Retrieves the size of all cells along the span of an axis. 227 /// For the X-axis, this retrieves the size of all columns. 228 /// For the Y-axis, this retrieves the size of all rows. 229 /// </summary> 230 /// <param name="axis">The axis span.</param> 231 /// <param name="spanLength">The absolute length of the span.</param> 232 /// <returns>The size of all cells along the span of <paramref name="axis"/>.</returns> 233 /// <exception cref="InvalidOperationException">If the <see cref="Dimension"/> for a cell is unsupported.</exception> 234 private float[] getCellSizesAlongAxis(Axes axis, float spanLength) 235 { 236 var spanDimensions = axis == Axes.X ? columnDimensions : rowDimensions; 237 int spanCount = axis == Axes.X ? cellColumns : cellRows; 238 239 var sizes = new float[spanCount]; 240 241 for (int i = 0; i < spanCount; i++) 242 { 243 if (i >= spanDimensions.Length) 244 break; 245 246 var dimension = spanDimensions[i]; 247 248 switch (dimension.Mode) 249 { 250 default: 251 throw new InvalidOperationException($"Unsupported dimension: {dimension.Mode}."); 252 253 case GridSizeMode.Distributed: 254 break; 255 256 case GridSizeMode.Relative: 257 sizes[i] = dimension.Size * spanLength; 258 break; 259 260 case GridSizeMode.Absolute: 261 sizes[i] = dimension.Size; 262 break; 263 264 case GridSizeMode.AutoSize: 265 float size = 0; 266 267 if (axis == Axes.X) 268 { 269 // Go through each row and get the width of the cell at the indexed column 270 for (int r = 0; r < cellRows; r++) 271 { 272 var cell = Content[r]?[i]; 273 if (cell == null || cell.RelativeSizeAxes.HasFlagFast(axis)) 274 continue; 275 276 size = Math.Max(size, getCellWidth(cell)); 277 } 278 } 279 else 280 { 281 // Go through each column and get the height of the cell at the indexed row 282 for (int c = 0; c < cellColumns; c++) 283 { 284 var cell = Content[i]?[c]; 285 if (cell == null || cell.RelativeSizeAxes.HasFlagFast(axis)) 286 continue; 287 288 size = Math.Max(size, getCellHeight(cell)); 289 } 290 } 291 292 sizes[i] = size; 293 break; 294 } 295 296 sizes[i] = Math.Clamp(sizes[i], dimension.MinSize, dimension.MaxSize); 297 } 298 299 return sizes; 300 } 301 302 private static bool shouldConsiderCell(Drawable cell) => cell != null && cell.IsAlive && cell.IsPresent; 303 private static float getCellWidth(Drawable cell) => shouldConsiderCell(cell) ? cell.BoundingBox.Width : 0; 304 private static float getCellHeight(Drawable cell) => shouldConsiderCell(cell) ? cell.BoundingBox.Height : 0; 305 306 /// <summary> 307 /// Distributes any available length along all distributed dimensions, if required. 308 /// </summary> 309 /// <param name="dimensions">The full dimensions of the row or column.</param> 310 /// <param name="spanLength">The total available length.</param> 311 /// <param name="cellSizes">An array containing pre-filled sizes of any non-distributed cells. This array will be mutated.</param> 312 /// <returns><paramref name="cellSizes"/>.</returns> 313 private float[] distribute(Dimension[] dimensions, float spanLength, float[] cellSizes) 314 { 315 // Indices of all distributed cells 316 int[] distributedIndices = Enumerable.Range(0, cellSizes.Length).Where(i => i >= dimensions.Length || dimensions[i].Mode == GridSizeMode.Distributed).ToArray(); 317 318 // The dimensions corresponding to all distributed cells 319 IEnumerable<DimensionEntry> distributedDimensions = distributedIndices.Select(i => new DimensionEntry(i, i >= dimensions.Length ? new Dimension() : dimensions[i])); 320 321 // Total number of distributed cells 322 int distributionCount = distributedIndices.Length; 323 324 // Non-distributed size 325 float requiredSize = cellSizes.Sum(); 326 327 // Distribution size for _each_ distributed cell 328 float distributionSize = Math.Max(0, spanLength - requiredSize) / distributionCount; 329 330 // Write the sizes of distributed cells. Ordering is important to maximize excess at every step 331 foreach (var entry in distributedDimensions.OrderBy(d => d.Dimension.Range)) 332 { 333 // Cells start off at their minimum size, and the total size should not exceed their maximum size 334 cellSizes[entry.Index] = Math.Min(entry.Dimension.MaxSize, entry.Dimension.MinSize + distributionSize); 335 336 // If there's no excess, any further distributions are guaranteed to also have no excess, so this becomes a null-op 337 // If there is an excess, the excess should be re-distributed among all other n-1 distributed cells 338 if (--distributionCount > 0) 339 distributionSize += Math.Max(0, distributionSize - entry.Dimension.Range) / distributionCount; 340 } 341 342 return cellSizes; 343 } 344 345 private readonly struct DimensionEntry 346 { 347 public readonly int Index; 348 public readonly Dimension Dimension; 349 350 public DimensionEntry(int index, Dimension dimension) 351 { 352 Index = index; 353 Dimension = dimension; 354 } 355 } 356 357 /// <summary> 358 /// Represents one cell of the <see cref="GridContainer"/>. 359 /// </summary> 360 private class CellContainer : Container 361 { 362 protected override bool OnInvalidate(Invalidation invalidation, InvalidationSource source) 363 { 364 var result = base.OnInvalidate(invalidation, source); 365 366 if (source == InvalidationSource.Child && (invalidation & (Invalidation.RequiredParentSizeToFit | Invalidation.Presence)) > 0) 367 result |= Parent?.Invalidate(invalidation, InvalidationSource.Child) ?? false; 368 369 return result; 370 } 371 } 372 } 373 374 /// <summary> 375 /// Defines the size of a row or column in a <see cref="GridContainer"/>. 376 /// </summary> 377 public class Dimension 378 { 379 /// <summary> 380 /// The mode in which this row or column <see cref="GridContainer"/> is sized. 381 /// </summary> 382 public readonly GridSizeMode Mode; 383 384 /// <summary> 385 /// The size of the row or column which this <see cref="Dimension"/> applies to. 386 /// Only has an effect if <see cref="Mode"/> is not <see cref="GridSizeMode.Distributed"/>. 387 /// </summary> 388 public readonly float Size; 389 390 /// <summary> 391 /// The minimum size of the row or column which this <see cref="Dimension"/> applies to. 392 /// </summary> 393 public readonly float MinSize; 394 395 /// <summary> 396 /// The maximum size of the row or column which this <see cref="Dimension"/> applies to. 397 /// </summary> 398 public readonly float MaxSize; 399 400 /// <summary> 401 /// Constructs a new <see cref="Dimension"/>. 402 /// </summary> 403 /// <param name="mode">The sizing mode to use.</param> 404 /// <param name="size">The size of this row or column. This only has an effect if <paramref name="mode"/> is not <see cref="GridSizeMode.Distributed"/>.</param> 405 /// <param name="minSize">The minimum size of this row or column.</param> 406 /// <param name="maxSize">The maximum size of this row or column.</param> 407 public Dimension(GridSizeMode mode = GridSizeMode.Distributed, float size = 0, float minSize = 0, float maxSize = float.MaxValue) 408 { 409 if (minSize < 0) 410 throw new ArgumentOutOfRangeException(nameof(minSize), "Must be greater than 0."); 411 412 if (minSize > maxSize) 413 throw new ArgumentOutOfRangeException(nameof(minSize), $"Must be less than {nameof(maxSize)}."); 414 415 Mode = mode; 416 Size = size; 417 MinSize = minSize; 418 MaxSize = maxSize; 419 } 420 421 /// <summary> 422 /// The range of the size of this <see cref="Dimension"/>. 423 /// </summary> 424 internal float Range => MaxSize - MinSize; 425 } 426 427 public enum GridSizeMode 428 { 429 /// <summary> 430 /// Any remaining area of the <see cref="GridContainer"/> will be divided amongst this and all 431 /// other elements which use <see cref="GridSizeMode.Distributed"/>. 432 /// </summary> 433 Distributed, 434 435 /// <summary> 436 /// This element should be sized relative to the dimensions of the <see cref="GridContainer"/>. 437 /// </summary> 438 Relative, 439 440 /// <summary> 441 /// This element has a size independent of the <see cref="GridContainer"/>. 442 /// </summary> 443 Absolute, 444 445 /// <summary> 446 /// This element will be sized to the maximum size along its span. 447 /// </summary> 448 AutoSize 449 } 450}