// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 Kybernetik //
#if UNITY_EDITOR
using System;
using UnityEditor;
using UnityEngine;
using static Animancer.Editor.AnimancerGUI;
namespace Animancer.Editor
{
/// [Editor-Only] Utility for drawing tables.
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/TableGUI
[Serializable]
public class TableGUI
{
/************************************************************************************************************************/
/// The pixel spacing between cells.
public static float Padding
=> 0;// StandardSpacing;
/// The style for the horizontal scroll bar.
public static GUIStyle HorizontalScrollBar
=> GUI.skin.horizontalScrollbar;
/// The style for the vertical scroll bar.
public static GUIStyle VerticalScrollBar
=> GUI.skin.verticalScrollbar;
/************************************************************************************************************************/
/// Draws the GUI for a specified cell.
/// `column` and `row` are given -1 for the labels.
public delegate void CellGUIDelegate(Rect area, int column, int row);
/// Draws the GUI for a specified cell.
/// `column` and `row` are given -1 for the labels.
public CellGUIDelegate DoCellGUI;
[NonSerialized] private Vector2 _MinCellSize;// Pixels.
[NonSerialized] private Vector2 _MaxCellSize;// Pixels.
[SerializeField] private Vector2 _LabelSize = new(0.25f, 0.25f);// Normalized.
[SerializeField] private Vector2 _ScrollPosition;
/// The minimum pixel size of each cell.
public ref Vector2 MinCellSize
=> ref _MinCellSize;
/// The maximum pixel size of each cell.
public ref Vector2 MaxCellSize
=> ref _MaxCellSize;
/// [] The normalized size of the header labels.
public ref Vector2 LabelSize
=> ref _LabelSize;
/// [] The position the table is currently scrolled to.
public ref Vector2 ScrollPosition
=> ref _ScrollPosition;
/************************************************************************************************************************/
/// Draws this table.
public void DoTableGUI(
Rect area,
int columns,
int rows)
{
HandleInput(area);
var scrollBarSize = new Vector2(
VerticalScrollBar.fixedWidth + Padding,
HorizontalScrollBar.fixedHeight + Padding);
area.size -= scrollBarSize;
CalculateSizes(area, columns, rows, out var labelSize, out var cellSize);
area.size += scrollBarSize;
var cornerArea = new Rect(area.position, labelSize + scrollBarSize);
DoCellGUI(cornerArea, -1, -1);
var labelResizerArea = cornerArea;
labelResizerArea.xMin = labelResizerArea.xMax - scrollBarSize.x;
labelResizerArea.yMin = labelResizerArea.yMax - scrollBarSize.y;
DoLabelResizerGUI(labelResizerArea, area);
var columnLabelArea = new Rect(
area.x + scrollBarSize.x + labelSize.x + Padding,
area.y,
cellSize.x,
labelSize.y);
var rowLabelArea = new Rect(
area.x,
area.y + scrollBarSize.y + labelSize.y + Padding,
labelSize.x,
cellSize.y);
area.xMin += labelSize.x + Padding + scrollBarSize.x;
area.yMin += labelSize.y + Padding + scrollBarSize.y;
GUI.BeginClip(area);
for (int x = 0; x < columns; x++)
{
for (int y = 0; y < rows; y++)
{
var cellArea = new Rect(
(cellSize.x + Padding) * x - _ScrollPosition.x,
(cellSize.y + Padding) * y - _ScrollPosition.y,
cellSize.x,
cellSize.y);
DoCellGUI(cellArea, x, y);
}
}
GUI.EndClip();
DrawColumnLabels(columnLabelArea, columns, area.width, scrollBarSize.y);
DrawRowLabels(rowLabelArea, rows, area.height, scrollBarSize.x);
}
/************************************************************************************************************************/
private static readonly int LabelResizerHint = "LabelResizer".GetHashCode();
private static GUIContent _LabelResizerIcon;
private void DoLabelResizerGUI(
Rect resizerArea,
Rect tableArea)
{
var control = new GUIControl(resizerArea, LabelResizerHint);
switch (control.EventType)
{
case EventType.MouseDown:
if (control.Event.button == 0 &&
control.TryUseMouseDown())
{
if (control.Event.clickCount == 2)
{
AutoSizeLabels(tableArea);
GUIUtility.hotControl = 0;
}
}
break;
case EventType.MouseUp:
control.TryUseMouseUp();
break;
case EventType.MouseDrag:
if (control.TryUseHotControl())
{
var offset = control.Event.mousePosition - tableArea.position;
LabelSize = new(
offset.x / tableArea.width,
offset.y / tableArea.height);
}
break;
case EventType.KeyDown:
if (control.TryUseKey(KeyCode.Escape))
Deselect();
break;
case EventType.Repaint:
EditorGUIUtility.AddCursorRect(resizerArea, MouseCursor.ResizeUpLeft);
AnimancerIcons.IconContent(ref _LabelResizerIcon, "MoveTool");
if (_LabelResizerIcon != null)
GUI.DrawTexture(resizerArea, _LabelResizerIcon.image);
break;
}
}
/************************************************************************************************************************/
private static readonly Matrix4x4
Rotate90LeftMatrix = Matrix4x4.Rotate(Quaternion.Euler(0, 0, -90));
private void DrawColumnLabels(Rect area, int count, float availableSize, float scrollSize)
{
var previousClip = GetGUIClipRect();
GUI.EndClip();
var totalSize =
area.width * count +
Padding * (count - 1);
var totalArea = new Rect(
area.x,
area.y + area.height + Padding,
availableSize,
scrollSize);
var translation = previousClip.position + area.position;
translation = -translation.GetPerpendicular();
translation.x -= area.height;
area.x = 0;
area.y = -_ScrollPosition.x;
(area.width, area.height) = (area.height, area.width);
GUI.BeginClip(new(0, 0, area.width, availableSize));
var matrix = GUI.matrix;
GUI.matrix =
Rotate90LeftMatrix *
Matrix4x4.Translate(translation);
for (int i = 0; i < count; i++)
{
DoCellGUI(area, i, -1);
area.y += area.height + Padding;
}
GUI.matrix = matrix;
GUI.EndClip();
GUI.BeginClip(previousClip);
var enabled = GUI.enabled;
GUI.enabled = availableSize < totalSize;
_ScrollPosition.x = GUI.HorizontalScrollbar(
totalArea,
_ScrollPosition.x,
availableSize,
0,
totalSize);
GUI.enabled = enabled;
}
/************************************************************************************************************************/
private void DrawRowLabels(Rect area, int count, float availableSize, float scrollSize)
{
var totalArea = area;
totalArea.height = availableSize;
GUI.BeginClip(totalArea);
area.x = 0;
area.y = -_ScrollPosition.y;
for (int i = 0; i < count; i++)
{
DoCellGUI(area, -1, i);
area.y += area.height + Padding;
}
GUI.EndClip();
var totalSize =
area.height * count +
Padding * (count - 1);
totalArea.x += totalArea.width + Padding;
totalArea.width = scrollSize;
var enabled = GUI.enabled;
GUI.enabled = availableSize < totalSize;
_ScrollPosition.y = GUI.VerticalScrollbar(
totalArea,
_ScrollPosition.y,
availableSize,
0,
totalSize);
GUI.enabled = enabled;
}
/************************************************************************************************************************/
private static readonly int ControlHint = nameof(TableGUI).GetHashCode();
private void HandleInput(Rect area)
{
var control = new GUIControl(area, ControlHint);
switch (control.EventType)
{
case EventType.ScrollWheel:
if (control.ContainsMousePosition)
{
var delta = control.Event.delta * 5;
if (control.Event.shift)
delta = delta.GetPerpendicular();
_ScrollPosition += delta;
control.Event.Use();
}
break;
case EventType.MouseDown:
if (control.Event.IsMiddleClick())
control.TryUseMouseDown();
break;
case EventType.MouseUp:
control.TryUseMouseUp();
break;
case EventType.MouseDrag:
if (control.TryUseHotControl())
_ScrollPosition -= control.Event.delta;
break;
}
}
/************************************************************************************************************************/
#region Size Calculation
/************************************************************************************************************************/
/// Calculates the current label and cell sizes for the given `area`.
public void CalculateSizes(
Rect area,
int columns,
int rows,
out Vector2 labelSize,
out Vector2 cellSize)
{
// Min cell size.
cellSize = _MinCellSize;
if (cellSize.x < 1)
cellSize.x = LineHeight;
if (cellSize.y < 1)
cellSize.y = LineHeight;
// Min label size.
labelSize.x = Mathf.Clamp(_LabelSize.x, 0, 0.9f);
labelSize.y = Mathf.Clamp(_LabelSize.y, 0, 0.9f);
labelSize = Vector2.Scale(area.size, labelSize);
if (labelSize == default)
labelSize = cellSize;
// Expand cells if there is more area available, up to the max.
var availableSize = area.size - labelSize;
cellSize.x = StretchCellSize(availableSize.x, cellSize.x, _MaxCellSize.x, columns);
cellSize.y = StretchCellSize(availableSize.y, cellSize.y, _MaxCellSize.y, rows);
// Expand labels if there is more area available.
labelSize.x = StretchLabelSize(area.width, labelSize.x, cellSize.x, columns);
labelSize.y = StretchLabelSize(area.height, labelSize.y, cellSize.y, rows);
}
/************************************************************************************************************************/
private static float StretchCellSize(
float availableSize,
float cellSize,
float maxCellSize,
int cellCount)
{
if (cellSize < maxCellSize)
{
availableSize -= Padding * (cellCount - 1);
if (availableSize > cellSize * cellCount)
cellSize = Math.Min(availableSize / cellCount, maxCellSize);
}
return cellSize;
}
/************************************************************************************************************************/
private static float StretchLabelSize(
float availableSize,
float labelSize,
float cellSize,
int cellCount)
{
labelSize = Math.Max(labelSize, availableSize - (cellSize + Padding) * cellCount);
labelSize = Math.Max(labelSize, cellSize);
return labelSize;
}
/************************************************************************************************************************/
/// A delegate to calculate the largest pixel width of the header labels.
public Func CalculateWidestLabel { get; set; }
private void AutoSizeLabels(Rect tableArea)
{
if (CalculateWidestLabel == null)
return;
var targetLabelSize = CalculateWidestLabel();
_LabelSize.x = targetLabelSize / tableArea.width;
_LabelSize.y = targetLabelSize / tableArea.height;
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
}
}
#endif