A game framework written with osu! in mind.
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}