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.Linq;
6using JetBrains.Annotations;
7using osu.Framework.Extensions;
8using osu.Framework.Graphics.Sprites;
9using osu.Framework.Localisation;
10
11namespace osu.Framework.Graphics.Containers
12{
13 /// <summary>
14 /// A container which tabulates <see cref="Drawable"/>s.
15 /// </summary>
16 public class TableContainer : CompositeDrawable
17 {
18 private readonly GridContainer grid;
19
20 public TableContainer()
21 {
22 InternalChild = grid = new GridContainer { RelativeSizeAxes = Axes.Both };
23 }
24
25 private Drawable[,] content;
26
27 /// <summary>
28 /// The content of this <see cref="TableContainer"/>, arranged in a 2D rectangular array.
29 /// <para>
30 /// Null elements are allowed to represent blank rows/cells.
31 /// </para>
32 /// </summary>
33 [CanBeNull]
34 public Drawable[,] Content
35 {
36 get => content;
37 set
38 {
39 if (content == value)
40 return;
41
42 content = value;
43
44 updateContent();
45 }
46 }
47
48 private TableColumn[] columns = Array.Empty<TableColumn>();
49
50 /// <summary>
51 /// Describes the columns of this <see cref="TableContainer"/>.
52 /// Each index of this array applies to the respective column index inside <see cref="Content"/>.
53 /// </summary>
54 public TableColumn[] Columns
55 {
56 [NotNull]
57 get => columns;
58 [CanBeNull]
59 set
60 {
61 value ??= Array.Empty<TableColumn>();
62
63 if (columns == value)
64 return;
65
66 columns = value;
67
68 updateContent();
69 }
70 }
71
72 private Dimension rowSize = new Dimension();
73
74 /// <summary>
75 /// Explicit dimensions for rows. The dimension is applied to every row of this <see cref="TableContainer"/>
76 /// </summary>
77 public Dimension RowSize
78 {
79 [NotNull]
80 get => rowSize;
81 [CanBeNull]
82 set
83 {
84 value ??= new Dimension();
85
86 if (rowSize == value)
87 return;
88
89 rowSize = value;
90
91 updateContent();
92 }
93 }
94
95 private bool showHeaders = true;
96
97 /// <summary>
98 /// Whether to display a row with column headers at the top of the table.
99 /// </summary>
100 public bool ShowHeaders
101 {
102 get => showHeaders;
103 set
104 {
105 if (showHeaders == value)
106 return;
107
108 showHeaders = value;
109
110 updateContent();
111 }
112 }
113
114 public override Axes RelativeSizeAxes
115 {
116 get => base.RelativeSizeAxes;
117 set
118 {
119 base.RelativeSizeAxes = value;
120 updateGridSize();
121 }
122 }
123
124 /// <summary>
125 /// Controls which <see cref="Axes"/> are automatically sized w.r.t. <see cref="CompositeDrawable.InternalChildren"/>.
126 /// Children's <see cref="Drawable.BypassAutoSizeAxes"/> are ignored for automatic sizing.
127 /// Most notably, <see cref="Drawable.RelativePositionAxes"/> and <see cref="Drawable.RelativeSizeAxes"/> of children
128 /// do not affect automatic sizing to avoid circular size dependencies.
129 /// It is not allowed to manually set <see cref="Drawable.Size"/> (or <see cref="Drawable.Width"/> / <see cref="Drawable.Height"/>)
130 /// on any <see cref="Axes"/> which are automatically sized.
131 /// </summary>
132 public new Axes AutoSizeAxes
133 {
134 get => base.AutoSizeAxes;
135 set
136 {
137 base.AutoSizeAxes = value;
138 updateGridSize();
139 }
140 }
141
142 /// <summary>
143 /// The total number of rows in the content, including the header.
144 /// </summary>
145 private int totalRows => (content?.GetLength(0) ?? 0) + (ShowHeaders ? 1 : 0);
146
147 /// <summary>
148 /// The total number of columns in the content, including the header.
149 /// </summary>
150 private int totalColumns
151 {
152 get
153 {
154 if (columns == null || !showHeaders)
155 return content?.GetLength(1) ?? 0;
156
157 return Math.Max(columns.Length, content?.GetLength(1) ?? 0);
158 }
159 }
160
161 /// <summary>
162 /// Adds content to the underlying grid.
163 /// </summary>
164 private void updateContent()
165 {
166 grid.Content = getContentWithHeaders().ToJagged();
167
168 grid.ColumnDimensions = columns.Select(c => c.Dimension).ToArray();
169 grid.RowDimensions = Enumerable.Repeat(RowSize, totalRows).ToArray();
170
171 updateAnchors();
172 }
173
174 /// <summary>
175 /// Adds headers, if required, and returns the resulting content. <see cref="content"/> is not modified in the process.
176 /// </summary>
177 /// <returns>The content, with headers added if required.</returns>
178 private Drawable[,] getContentWithHeaders()
179 {
180 if (!ShowHeaders || Columns == null || Columns.Length == 0)
181 return content;
182
183 int rowCount = totalRows;
184 int columnCount = totalColumns;
185
186 var result = new Drawable[rowCount, columnCount];
187
188 for (int row = 0; row < rowCount; row++)
189 {
190 for (int col = 0; col < columnCount; col++)
191 {
192 if (row == 0)
193 result[row, col] = CreateHeader(col, col >= Columns?.Length ? null : Columns?[col]);
194 else if (col < content.GetLength(1))
195 result[row, col] = content[row - 1, col];
196 }
197 }
198
199 return result;
200 }
201
202 /// <summary>
203 /// Ensures that all cells have the correct anchors defined by <see cref="Columns"/>.
204 /// </summary>
205 private void updateAnchors()
206 {
207 if (grid.Content == null)
208 return;
209
210 int rowCount = totalRows;
211 int columnCount = totalColumns;
212
213 for (int row = 0; row < rowCount; row++)
214 {
215 for (int col = 0; col < columnCount; col++)
216 {
217 if (col >= columns.Length)
218 break;
219
220 Drawable child = grid.Content[row][col];
221
222 if (child == null)
223 continue;
224
225 child.Origin = columns[col].Anchor;
226 child.Anchor = columns[col].Anchor;
227 }
228 }
229 }
230
231 /// <summary>
232 /// Keeps the grid autosized in our autosized axes, and relative-sized in our non-autosized axes.
233 /// </summary>
234 private void updateGridSize()
235 {
236 grid.RelativeSizeAxes = Axes.None;
237 grid.AutoSizeAxes = Axes.None;
238
239 if ((AutoSizeAxes & Axes.X) == 0)
240 grid.RelativeSizeAxes |= Axes.X;
241 else
242 grid.AutoSizeAxes |= Axes.X;
243
244 if ((AutoSizeAxes & Axes.Y) == 0)
245 grid.RelativeSizeAxes |= Axes.Y;
246 else
247 grid.AutoSizeAxes |= Axes.Y;
248 }
249
250 /// <summary>
251 /// Creates the content for a cell in the header row of the table.
252 /// </summary>
253 /// <param name="index">The column index.</param>
254 /// <param name="column">The column definition.</param>
255 /// <returns>The cell content.</returns>
256 protected virtual Drawable CreateHeader(int index, [CanBeNull] TableColumn column) => new SpriteText { Text = column?.Header ?? string.Empty };
257 }
258
259 /// <summary>
260 /// Defines a column of the <see cref="TableContainer"/>.
261 /// </summary>
262 public class TableColumn
263 {
264 /// <summary>
265 /// The localisable text to be displayed in the cell.
266 /// </summary>
267 public readonly LocalisableString Header;
268
269 /// <summary>
270 /// The anchor of all cells in this column of the <see cref="TableContainer"/>.
271 /// </summary>
272 public readonly Anchor Anchor;
273
274 /// <summary>
275 /// The dimension of the column in the table.
276 /// </summary>
277 public readonly Dimension Dimension;
278
279 /// <summary>
280 /// Constructs a new <see cref="TableColumn"/>.
281 /// </summary>
282 /// <param name="header">The localisable text to be displayed in the cell.</param>
283 /// <param name="anchor">The anchor of all cells in this column of the <see cref="TableContainer"/>.</param>
284 /// <param name="dimension">The dimension of the column in the table.</param>
285 public TableColumn(LocalisableString? header = null, Anchor anchor = Anchor.TopLeft, Dimension dimension = null)
286 {
287 Header = header ?? string.Empty;
288 Anchor = anchor;
289 Dimension = dimension ?? new Dimension();
290 }
291 }
292}