TableContainer.cs 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285
  1. using System.Numerics;
  2. using Robust.Client.UserInterface.Controls;
  3. namespace Content.Client.UserInterface.Controls;
  4. // This control is not part of engine because I quickly wrote it in 2 hours at 2 AM and don't want to deal with
  5. // API stabilization and/or figuring out relation to GridContainer.
  6. // Grid layout is a complicated problem and I don't want to commit another half-baked thing into the engine.
  7. // It's probably sufficient for its use case (RichTextLabel tables for rules/guidebook).
  8. // Despite that, it's still better comment the shit half of you write on a regular basis.
  9. //
  10. // EMO: thank you PJB i was going to kill myself.
  11. /// <summary>
  12. /// Displays children in a tabular grid. Unlike <see cref="GridContainer"/>,
  13. /// properly handles layout constraints so putting word-wrapping <see cref="RichTextLabel"/> in it should work.
  14. /// </summary>
  15. /// <remarks>
  16. /// All children are automatically laid out in <see cref="Columns"/> columns.
  17. /// The first control is in the top left, laid out per row from there.
  18. /// </remarks>
  19. [Virtual]
  20. public class TableContainer : Container
  21. {
  22. private int _columns = 1;
  23. /// <summary>
  24. /// The absolute minimum width a column can be forced to.
  25. /// </summary>
  26. /// <remarks>
  27. /// <para>
  28. /// If a column *asks* for less width than this (small contents), it can still be smaller.
  29. /// But if it asks for more it cannot go below this width.
  30. /// </para>
  31. /// </remarks>
  32. public float MinForcedColumnWidth { get; set; } = 50;
  33. // Scratch space used while calculating layout, cached to avoid regular allocations during layout pass.
  34. private ColumnData[] _columnDataCache = [];
  35. private RowData[] _rowDataCache = [];
  36. /// <summary>
  37. /// How many columns should be displayed.
  38. /// </summary>
  39. public int Columns
  40. {
  41. get => _columns;
  42. set
  43. {
  44. ArgumentOutOfRangeException.ThrowIfLessThan(value, 1, nameof(value));
  45. _columns = value;
  46. }
  47. }
  48. protected override Vector2 MeasureOverride(Vector2 availableSize)
  49. {
  50. ResetCachedArrays();
  51. // Do a first pass measuring all child controls as if they're given infinite space.
  52. // This gives us a maximum width the columns want, which we use to proportion them later.
  53. var columnIdx = 0;
  54. foreach (var child in Children)
  55. {
  56. ref var column = ref _columnDataCache[columnIdx];
  57. child.Measure(new Vector2(float.PositiveInfinity, float.PositiveInfinity));
  58. column.MaxWidth = Math.Max(column.MaxWidth, child.DesiredSize.X);
  59. columnIdx += 1;
  60. if (columnIdx == _columns)
  61. columnIdx = 0;
  62. }
  63. // Calculate Slack and MinWidth for all columns. Also calculate sums for all columns.
  64. var totalMinWidth = 0f;
  65. var totalMaxWidth = 0f;
  66. var totalSlack = 0f;
  67. for (var c = 0; c < _columns; c++)
  68. {
  69. ref var column = ref _columnDataCache[c];
  70. column.MinWidth = Math.Min(column.MaxWidth, MinForcedColumnWidth);
  71. column.Slack = column.MaxWidth - column.MinWidth;
  72. totalMinWidth += column.MinWidth;
  73. totalMaxWidth += column.MaxWidth;
  74. totalSlack += column.Slack;
  75. }
  76. if (totalMaxWidth <= availableSize.X)
  77. {
  78. // We want less horizontal space than we're given. Huh, that's convenient.
  79. // Just set assigned width to be however much they asked for.
  80. // We could probably skip the second measure pass in this scenario,
  81. // but that's just an optimization, so I don't care right now.
  82. //
  83. // There's probably a very clever way to make this behavior work with the else block of logic,
  84. // just by fiddling with the math.
  85. // I'm dumb, it's 4:30 AM. Yeah, I *started* at 2 AM.
  86. for (var c = 0; c < _columns; c++)
  87. {
  88. ref var column = ref _columnDataCache[c];
  89. column.AssignedWidth = column.MaxWidth;
  90. }
  91. }
  92. else
  93. {
  94. // We don't have enough horizontal space,
  95. // at least without causing *some* sort of word wrapping (assuming text contents).
  96. //
  97. // Assign horizontal space proportional to the wanted maximum size of the columns.
  98. var assignableWidth = Math.Max(0, availableSize.X - totalMinWidth);
  99. for (var c = 0; c < _columns; c++)
  100. {
  101. ref var column = ref _columnDataCache[c];
  102. var slackRatio = column.Slack / totalSlack;
  103. column.AssignedWidth = column.MinWidth + slackRatio * assignableWidth;
  104. }
  105. }
  106. // Go over controls for a second measuring pass, this time giving them their assigned measure width.
  107. // This will give us a height to slot into per-row data.
  108. // We still measure assuming infinite vertical space.
  109. // This control can't properly handle being constrained on the Y axis.
  110. columnIdx = 0;
  111. var rowIdx = 0;
  112. foreach (var child in Children)
  113. {
  114. ref var column = ref _columnDataCache[columnIdx];
  115. ref var row = ref _rowDataCache[rowIdx];
  116. child.Measure(new Vector2(column.AssignedWidth, float.PositiveInfinity));
  117. row.MeasuredHeight = Math.Max(row.MeasuredHeight, child.DesiredSize.Y);
  118. columnIdx += 1;
  119. if (columnIdx == _columns)
  120. {
  121. columnIdx = 0;
  122. rowIdx += 1;
  123. }
  124. }
  125. // Sum up height of all rows to get final measured table height.
  126. var totalHeight = 0f;
  127. for (var r = 0; r < _rowDataCache.Length; r++)
  128. {
  129. ref var row = ref _rowDataCache[r];
  130. totalHeight += row.MeasuredHeight;
  131. }
  132. return new Vector2(Math.Min(availableSize.X, totalMaxWidth), totalHeight);
  133. }
  134. protected override Vector2 ArrangeOverride(Vector2 finalSize)
  135. {
  136. // TODO: Expand to fit given vertical space.
  137. // Calculate MinWidth and Slack sums again from column data.
  138. // We could've cached these from measure but whatever.
  139. var totalMinWidth = 0f;
  140. var totalSlack = 0f;
  141. for (var c = 0; c < _columns; c++)
  142. {
  143. ref var column = ref _columnDataCache[c];
  144. totalMinWidth += column.MinWidth;
  145. totalSlack += column.Slack;
  146. }
  147. // Calculate new width based on final given size, also assign horizontal positions of all columns.
  148. var assignableWidth = Math.Max(0, finalSize.X - totalMinWidth);
  149. var xPos = 0f;
  150. for (var c = 0; c < _columns; c++)
  151. {
  152. ref var column = ref _columnDataCache[c];
  153. var slackRatio = column.Slack / totalSlack;
  154. column.ArrangedWidth = column.MinWidth + slackRatio * assignableWidth;
  155. column.ArrangedX = xPos;
  156. xPos += column.ArrangedWidth;
  157. }
  158. // Do actual arrangement row-by-row.
  159. var arrangeY = 0f;
  160. for (var r = 0; r < _rowDataCache.Length; r++)
  161. {
  162. ref var row = ref _rowDataCache[r];
  163. for (var c = 0; c < _columns; c++)
  164. {
  165. ref var column = ref _columnDataCache[c];
  166. var index = c + r * _columns;
  167. if (index >= ChildCount) // Quit early if we don't actually fill out the row.
  168. break;
  169. var child = GetChild(c + r * _columns);
  170. child.Arrange(UIBox2.FromDimensions(column.ArrangedX, arrangeY, column.ArrangedWidth, row.MeasuredHeight));
  171. }
  172. arrangeY += row.MeasuredHeight;
  173. }
  174. return finalSize with { Y = arrangeY };
  175. }
  176. /// <summary>
  177. /// Ensure cached array space is allocated to correct size and is reset to a clean slate.
  178. /// </summary>
  179. private void ResetCachedArrays()
  180. {
  181. // 1-argument Array.Clear() is not currently available in sandbox (added in .NET 6).
  182. if (_columnDataCache.Length != _columns)
  183. _columnDataCache = new ColumnData[_columns];
  184. Array.Clear(_columnDataCache, 0, _columnDataCache.Length);
  185. var rowCount = ChildCount / _columns;
  186. if (ChildCount % _columns != 0)
  187. rowCount += 1;
  188. if (rowCount != _rowDataCache.Length)
  189. _rowDataCache = new RowData[rowCount];
  190. Array.Clear(_rowDataCache, 0, _rowDataCache.Length);
  191. }
  192. /// <summary>
  193. /// Per-column data used during layout.
  194. /// </summary>
  195. private struct ColumnData
  196. {
  197. // Measure data.
  198. /// <summary>
  199. /// The maximum width any control in this column wants, if given infinite space.
  200. /// Maximum of all controls on the column.
  201. /// </summary>
  202. public float MaxWidth;
  203. /// <summary>
  204. /// The minimum width this column may be given.
  205. /// This is either <see cref="MaxWidth"/> or <see cref="TableContainer.MinForcedColumnWidth"/>.
  206. /// </summary>
  207. public float MinWidth;
  208. /// <summary>
  209. /// Difference between max and min width; how much this column can expand from its minimum.
  210. /// </summary>
  211. public float Slack;
  212. /// <summary>
  213. /// How much horizontal space this column was assigned at measure time.
  214. /// </summary>
  215. public float AssignedWidth;
  216. // Arrange data.
  217. /// <summary>
  218. /// How much horizontal space this column was assigned at arrange time.
  219. /// </summary>
  220. public float ArrangedWidth;
  221. /// <summary>
  222. /// The horizontal position this column was assigned at arrange time.
  223. /// </summary>
  224. public float ArrangedX;
  225. }
  226. private struct RowData
  227. {
  228. // Measure data.
  229. /// <summary>
  230. /// How much height the tallest control on this row was measured at,
  231. /// measuring for infinite vertical space but assigned column width.
  232. /// </summary>
  233. public float MeasuredHeight;
  234. }
  235. }