TableGUI.cs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415
  1. // Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 Kybernetik //
  2. #if UNITY_EDITOR
  3. using System;
  4. using UnityEditor;
  5. using UnityEngine;
  6. using static Animancer.Editor.AnimancerGUI;
  7. namespace Animancer.Editor
  8. {
  9. /// <summary>[Editor-Only] Utility for drawing tables.</summary>
  10. /// https://kybernetik.com.au/animancer/api/Animancer.Editor/TableGUI
  11. [Serializable]
  12. public class TableGUI
  13. {
  14. /************************************************************************************************************************/
  15. /// <summary>The pixel spacing between cells.</summary>
  16. public static float Padding
  17. => 0;// StandardSpacing;
  18. /// <summary>The style for the horizontal scroll bar.</summary>
  19. public static GUIStyle HorizontalScrollBar
  20. => GUI.skin.horizontalScrollbar;
  21. /// <summary>The style for the vertical scroll bar.</summary>
  22. public static GUIStyle VerticalScrollBar
  23. => GUI.skin.verticalScrollbar;
  24. /************************************************************************************************************************/
  25. /// <summary>Draws the GUI for a specified cell.</summary>
  26. /// <remarks>`column` and `row` are given <c>-1</c> for the labels.</remarks>
  27. public delegate void CellGUIDelegate(Rect area, int column, int row);
  28. /// <summary>Draws the GUI for a specified cell.</summary>
  29. /// <remarks>`column` and `row` are given <c>-1</c> for the labels.</remarks>
  30. public CellGUIDelegate DoCellGUI;
  31. [NonSerialized] private Vector2 _MinCellSize;// Pixels.
  32. [NonSerialized] private Vector2 _MaxCellSize;// Pixels.
  33. [SerializeField] private Vector2 _LabelSize = new(0.25f, 0.25f);// Normalized.
  34. [SerializeField] private Vector2 _ScrollPosition;
  35. /// <summary>The minimum pixel size of each cell.</summary>
  36. public ref Vector2 MinCellSize
  37. => ref _MinCellSize;
  38. /// <summary>The maximum pixel size of each cell.</summary>
  39. public ref Vector2 MaxCellSize
  40. => ref _MaxCellSize;
  41. /// <summary>[<see cref="SerializeField"/>] The normalized size of the header labels.</summary>
  42. public ref Vector2 LabelSize
  43. => ref _LabelSize;
  44. /// <summary>[<see cref="SerializeField"/>] The position the table is currently scrolled to.</summary>
  45. public ref Vector2 ScrollPosition
  46. => ref _ScrollPosition;
  47. /************************************************************************************************************************/
  48. /// <summary>Draws this table.</summary>
  49. public void DoTableGUI(
  50. Rect area,
  51. int columns,
  52. int rows)
  53. {
  54. HandleInput(area);
  55. var scrollBarSize = new Vector2(
  56. VerticalScrollBar.fixedWidth + Padding,
  57. HorizontalScrollBar.fixedHeight + Padding);
  58. area.size -= scrollBarSize;
  59. CalculateSizes(area, columns, rows, out var labelSize, out var cellSize);
  60. area.size += scrollBarSize;
  61. var cornerArea = new Rect(area.position, labelSize + scrollBarSize);
  62. DoCellGUI(cornerArea, -1, -1);
  63. var labelResizerArea = cornerArea;
  64. labelResizerArea.xMin = labelResizerArea.xMax - scrollBarSize.x;
  65. labelResizerArea.yMin = labelResizerArea.yMax - scrollBarSize.y;
  66. DoLabelResizerGUI(labelResizerArea, area);
  67. var columnLabelArea = new Rect(
  68. area.x + scrollBarSize.x + labelSize.x + Padding,
  69. area.y,
  70. cellSize.x,
  71. labelSize.y);
  72. var rowLabelArea = new Rect(
  73. area.x,
  74. area.y + scrollBarSize.y + labelSize.y + Padding,
  75. labelSize.x,
  76. cellSize.y);
  77. area.xMin += labelSize.x + Padding + scrollBarSize.x;
  78. area.yMin += labelSize.y + Padding + scrollBarSize.y;
  79. GUI.BeginClip(area);
  80. for (int x = 0; x < columns; x++)
  81. {
  82. for (int y = 0; y < rows; y++)
  83. {
  84. var cellArea = new Rect(
  85. (cellSize.x + Padding) * x - _ScrollPosition.x,
  86. (cellSize.y + Padding) * y - _ScrollPosition.y,
  87. cellSize.x,
  88. cellSize.y);
  89. DoCellGUI(cellArea, x, y);
  90. }
  91. }
  92. GUI.EndClip();
  93. DrawColumnLabels(columnLabelArea, columns, area.width, scrollBarSize.y);
  94. DrawRowLabels(rowLabelArea, rows, area.height, scrollBarSize.x);
  95. }
  96. /************************************************************************************************************************/
  97. private static readonly int LabelResizerHint = "LabelResizer".GetHashCode();
  98. private static GUIContent _LabelResizerIcon;
  99. private void DoLabelResizerGUI(
  100. Rect resizerArea,
  101. Rect tableArea)
  102. {
  103. var control = new GUIControl(resizerArea, LabelResizerHint);
  104. switch (control.EventType)
  105. {
  106. case EventType.MouseDown:
  107. if (control.Event.button == 0 &&
  108. control.TryUseMouseDown())
  109. {
  110. if (control.Event.clickCount == 2)
  111. {
  112. AutoSizeLabels(tableArea);
  113. GUIUtility.hotControl = 0;
  114. }
  115. }
  116. break;
  117. case EventType.MouseUp:
  118. control.TryUseMouseUp();
  119. break;
  120. case EventType.MouseDrag:
  121. if (control.TryUseHotControl())
  122. {
  123. var offset = control.Event.mousePosition - tableArea.position;
  124. LabelSize = new(
  125. offset.x / tableArea.width,
  126. offset.y / tableArea.height);
  127. }
  128. break;
  129. case EventType.KeyDown:
  130. if (control.TryUseKey(KeyCode.Escape))
  131. Deselect();
  132. break;
  133. case EventType.Repaint:
  134. EditorGUIUtility.AddCursorRect(resizerArea, MouseCursor.ResizeUpLeft);
  135. AnimancerIcons.IconContent(ref _LabelResizerIcon, "MoveTool");
  136. if (_LabelResizerIcon != null)
  137. GUI.DrawTexture(resizerArea, _LabelResizerIcon.image);
  138. break;
  139. }
  140. }
  141. /************************************************************************************************************************/
  142. private static readonly Matrix4x4
  143. Rotate90LeftMatrix = Matrix4x4.Rotate(Quaternion.Euler(0, 0, -90));
  144. private void DrawColumnLabels(Rect area, int count, float availableSize, float scrollSize)
  145. {
  146. var previousClip = GetGUIClipRect();
  147. GUI.EndClip();
  148. var totalSize =
  149. area.width * count +
  150. Padding * (count - 1);
  151. var totalArea = new Rect(
  152. area.x,
  153. area.y + area.height + Padding,
  154. availableSize,
  155. scrollSize);
  156. var translation = previousClip.position + area.position;
  157. translation = -translation.GetPerpendicular();
  158. translation.x -= area.height;
  159. area.x = 0;
  160. area.y = -_ScrollPosition.x;
  161. (area.width, area.height) = (area.height, area.width);
  162. GUI.BeginClip(new(0, 0, area.width, availableSize));
  163. var matrix = GUI.matrix;
  164. GUI.matrix =
  165. Rotate90LeftMatrix *
  166. Matrix4x4.Translate(translation);
  167. for (int i = 0; i < count; i++)
  168. {
  169. DoCellGUI(area, i, -1);
  170. area.y += area.height + Padding;
  171. }
  172. GUI.matrix = matrix;
  173. GUI.EndClip();
  174. GUI.BeginClip(previousClip);
  175. var enabled = GUI.enabled;
  176. GUI.enabled = availableSize < totalSize;
  177. _ScrollPosition.x = GUI.HorizontalScrollbar(
  178. totalArea,
  179. _ScrollPosition.x,
  180. availableSize,
  181. 0,
  182. totalSize);
  183. GUI.enabled = enabled;
  184. }
  185. /************************************************************************************************************************/
  186. private void DrawRowLabels(Rect area, int count, float availableSize, float scrollSize)
  187. {
  188. var totalArea = area;
  189. totalArea.height = availableSize;
  190. GUI.BeginClip(totalArea);
  191. area.x = 0;
  192. area.y = -_ScrollPosition.y;
  193. for (int i = 0; i < count; i++)
  194. {
  195. DoCellGUI(area, -1, i);
  196. area.y += area.height + Padding;
  197. }
  198. GUI.EndClip();
  199. var totalSize =
  200. area.height * count +
  201. Padding * (count - 1);
  202. totalArea.x += totalArea.width + Padding;
  203. totalArea.width = scrollSize;
  204. var enabled = GUI.enabled;
  205. GUI.enabled = availableSize < totalSize;
  206. _ScrollPosition.y = GUI.VerticalScrollbar(
  207. totalArea,
  208. _ScrollPosition.y,
  209. availableSize,
  210. 0,
  211. totalSize);
  212. GUI.enabled = enabled;
  213. }
  214. /************************************************************************************************************************/
  215. private static readonly int ControlHint = nameof(TableGUI).GetHashCode();
  216. private void HandleInput(Rect area)
  217. {
  218. var control = new GUIControl(area, ControlHint);
  219. switch (control.EventType)
  220. {
  221. case EventType.ScrollWheel:
  222. if (control.ContainsMousePosition)
  223. {
  224. var delta = control.Event.delta * 5;
  225. if (control.Event.shift)
  226. delta = delta.GetPerpendicular();
  227. _ScrollPosition += delta;
  228. control.Event.Use();
  229. }
  230. break;
  231. case EventType.MouseDown:
  232. if (control.Event.IsMiddleClick())
  233. control.TryUseMouseDown();
  234. break;
  235. case EventType.MouseUp:
  236. control.TryUseMouseUp();
  237. break;
  238. case EventType.MouseDrag:
  239. if (control.TryUseHotControl())
  240. _ScrollPosition -= control.Event.delta;
  241. break;
  242. }
  243. }
  244. /************************************************************************************************************************/
  245. #region Size Calculation
  246. /************************************************************************************************************************/
  247. /// <summary>Calculates the current label and cell sizes for the given `area`.</summary>
  248. public void CalculateSizes(
  249. Rect area,
  250. int columns,
  251. int rows,
  252. out Vector2 labelSize,
  253. out Vector2 cellSize)
  254. {
  255. // Min cell size.
  256. cellSize = _MinCellSize;
  257. if (cellSize.x < 1)
  258. cellSize.x = LineHeight;
  259. if (cellSize.y < 1)
  260. cellSize.y = LineHeight;
  261. // Min label size.
  262. labelSize.x = Mathf.Clamp(_LabelSize.x, 0, 0.9f);
  263. labelSize.y = Mathf.Clamp(_LabelSize.y, 0, 0.9f);
  264. labelSize = Vector2.Scale(area.size, labelSize);
  265. if (labelSize == default)
  266. labelSize = cellSize;
  267. // Expand cells if there is more area available, up to the max.
  268. var availableSize = area.size - labelSize;
  269. cellSize.x = StretchCellSize(availableSize.x, cellSize.x, _MaxCellSize.x, columns);
  270. cellSize.y = StretchCellSize(availableSize.y, cellSize.y, _MaxCellSize.y, rows);
  271. // Expand labels if there is more area available.
  272. labelSize.x = StretchLabelSize(area.width, labelSize.x, cellSize.x, columns);
  273. labelSize.y = StretchLabelSize(area.height, labelSize.y, cellSize.y, rows);
  274. }
  275. /************************************************************************************************************************/
  276. private static float StretchCellSize(
  277. float availableSize,
  278. float cellSize,
  279. float maxCellSize,
  280. int cellCount)
  281. {
  282. if (cellSize < maxCellSize)
  283. {
  284. availableSize -= Padding * (cellCount - 1);
  285. if (availableSize > cellSize * cellCount)
  286. cellSize = Math.Min(availableSize / cellCount, maxCellSize);
  287. }
  288. return cellSize;
  289. }
  290. /************************************************************************************************************************/
  291. private static float StretchLabelSize(
  292. float availableSize,
  293. float labelSize,
  294. float cellSize,
  295. int cellCount)
  296. {
  297. labelSize = Math.Max(labelSize, availableSize - (cellSize + Padding) * cellCount);
  298. labelSize = Math.Max(labelSize, cellSize);
  299. return labelSize;
  300. }
  301. /************************************************************************************************************************/
  302. /// <summary>A delegate to calculate the largest pixel width of the header labels.</summary>
  303. public Func<float> CalculateWidestLabel { get; set; }
  304. private void AutoSizeLabels(Rect tableArea)
  305. {
  306. if (CalculateWidestLabel == null)
  307. return;
  308. var targetLabelSize = CalculateWidestLabel();
  309. _LabelSize.x = targetLabelSize / tableArea.width;
  310. _LabelSize.y = targetLabelSize / tableArea.height;
  311. }
  312. /************************************************************************************************************************/
  313. #endregion
  314. /************************************************************************************************************************/
  315. }
  316. }
  317. #endif