A game framework written with osu! in mind.
at master 292 lines 9.3 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.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}