// Animancer // Copyright 2020 Kybernetik //
#if UNITY_EDITOR
using System;
using System.Collections;
using UnityEditor;
using UnityEngine;
namespace Animancer.Editor
{
/// [Editor-Only] Various GUI utilities used throughout Animancer.
public static class AnimancerGUI
{
/************************************************************************************************************************/
#region Standard Values
/************************************************************************************************************************/
/// The highlight color used for fields showing a warning.
public static readonly Color
WarningFieldColor = new Color(1, 0.9f, 0.6f);
/// The highlight color used for fields showing an error.
public static readonly Color
ErrorFieldColor = new Color(1, 0.6f, 0.6f);
/************************************************************************************************************************/
/// set to false.
public static readonly GUILayoutOption[]
DontExpandWidth = { GUILayout.ExpandWidth(false) };
/************************************************************************************************************************/
/// Wrapper around .
public static float LineHeight
{
get { return EditorGUIUtility.singleLineHeight; }
}
/************************************************************************************************************************/
/// Wrapper around .
public static float StandardSpacing
{
get { return EditorGUIUtility.standardVerticalSpacing; }
}
/************************************************************************************************************************/
private static float _IndentSize = -1;
///
/// The number of pixels of indentation for each increment.
///
public static float IndentSize
{
get
{
if (_IndentSize < 0)
{
var indentLevel = EditorGUI.indentLevel;
EditorGUI.indentLevel = 1;
_IndentSize = EditorGUI.IndentedRect(new Rect()).x;
EditorGUI.indentLevel = indentLevel;
}
return _IndentSize;
}
}
/************************************************************************************************************************/
private static float _ToggleWidth = -1;
/// The width of a standard with no label.
public static float ToggleWidth
{
get
{
if (_ToggleWidth == -1)
_ToggleWidth = GUI.skin.toggle.CalculateWidth(GUIContent.none);
return _ToggleWidth;
}
}
/************************************************************************************************************************/
/// The color of the standard label text.
public static Color TextColor
{
get { return GUI.skin.label.normal.textColor; }
}
/************************************************************************************************************************/
///
/// A more compact with a fixed size as a tiny box.
///
public static readonly GUIStyle MiniButton = new GUIStyle(EditorStyles.miniButton)
{
margin = new RectOffset(0, 0, 2, 0),
padding = new RectOffset(2, 3, 2, 2),
alignment = TextAnchor.MiddleCenter,
fixedHeight = LineHeight,
fixedWidth = LineHeight - 1
};
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Layout
/************************************************************************************************************************/
/// Indicates where should add the .
public enum SpacingMode
{
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member.
None,
Before,
After,
BeforeAndAfter
#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member.
}
///
/// Uses to get a occupying a single
/// standard line with the added according to the specified `spacing`.
///
public static Rect LayoutSingleLineRect(SpacingMode spacing = SpacingMode.None)
{
Rect rect;
switch (spacing)
{
case SpacingMode.None:
return GUILayoutUtility.GetRect(0, LineHeight);
case SpacingMode.Before:
rect = GUILayoutUtility.GetRect(0, LineHeight + StandardSpacing);
rect.yMin += StandardSpacing;
return rect;
case SpacingMode.After:
rect = GUILayoutUtility.GetRect(0, LineHeight + StandardSpacing);
rect.yMax -= StandardSpacing;
return rect;
case SpacingMode.BeforeAndAfter:
rect = GUILayoutUtility.GetRect(0, LineHeight + StandardSpacing * 2);
rect.yMin += StandardSpacing;
rect.yMax -= StandardSpacing;
return rect;
default:
throw new ArgumentException("Unknown StandardSpacing: " + spacing, "spacing");
}
}
/************************************************************************************************************************/
///
/// If the is positive, this method moves the by that amount and
/// adds the .
///
public static void NextVerticalArea(ref Rect area)
{
if (area.height > 0)
area.y += area.height + StandardSpacing;
}
/************************************************************************************************************************/
///
/// Subtracts the `width` from the left side of the `area` and returns a new occupying the
/// removed section.
///
public static Rect StealFromLeft(ref Rect area, float width, float padding = 0)
{
var newRect = new Rect(area.x, area.y, width, area.height);
area.xMin += width + padding;
return newRect;
}
///
/// Subtracts the `width` from the right side of the `area` and returns a new occupying the
/// removed section.
///
public static Rect StealFromRight(ref Rect area, float width, float padding = 0)
{
area.width -= width + padding;
return new Rect(area.xMax + padding, area.y, width, area.height);
}
/************************************************************************************************************************/
///
/// Divides the given `area` such that the fields associated with both labels will have equal space
/// remaining after the labels themselves.
///
public static void SplitHorizontally(Rect area, string label0, string label1,
out float width0, out float width1, out Rect rect0, out Rect rect1)
{
width0 = CalculateLabelWidth(label0);
width1 = CalculateLabelWidth(label1);
const float Padding = 1;
rect0 = rect1 = area;
var remainingWidth = area.width - width0 - width1 - Padding;
rect0.width = width0 + remainingWidth * 0.5f;
rect1.xMin = rect0.xMax + Padding;
}
/************************************************************************************************************************/
///
/// Calls and returns the max width.
///
public static float CalculateWidth(this GUIStyle style, GUIContent content)
{
float _, width;
style.CalcMinMaxWidth(content, out _, out width);
return width;
}
///
/// Calls and returns the max width.
///
/// This method uses the .
///
public static float CalculateWidth(this GUIStyle style, string content)
{
return style.CalculateWidth(TempContent(content));
}
/************************************************************************************************************************/
///
/// Creates a for calculating the GUI width occupied by text using the
/// specified `style`.
///
public static ConversionCache CreateWidthCache(GUIStyle style)
{
return new ConversionCache((text) => style.CalculateWidth(text));
}
/************************************************************************************************************************/
private static ConversionCache _LabelWidthCache;
///
/// Calls using and returns the max
/// width. The result is cached for efficient reuse.
///
/// This method uses the .
///
public static float CalculateLabelWidth(string text)
{
if (_LabelWidthCache == null)
_LabelWidthCache = CreateWidthCache(GUI.skin.label);
return _LabelWidthCache.Convert(text);
}
/************************************************************************************************************************/
///
/// Begins a vertical layout group using the given style and decreases the
/// to compensate for the indentation.
///
public static void BeginVerticalBox(GUIStyle style)
{
if (style == null)
{
GUILayout.BeginVertical();
return;
}
GUILayout.BeginVertical(style);
EditorGUIUtility.labelWidth -= style.padding.left;
}
///
/// Ends a layout group started by and restores the
/// .
///
public static void EndVerticalBox(GUIStyle style)
{
if (style != null)
EditorGUIUtility.labelWidth += style.padding.left;
GUILayout.EndVertical();
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Labels
/************************************************************************************************************************/
/// Used by .
private static GUIContent _TempContent;
///
/// Returns a with the specified parameters. The same instance is returned by
/// every subsequent call.
///
public static GUIContent TempContent(string text = null, string tooltip = null, bool narrowText = true)
{
if (_TempContent == null)
_TempContent = new GUIContent();
if (narrowText)
text = GetNarrowText(text);
_TempContent.text = text;
_TempContent.tooltip = tooltip;
return _TempContent;
}
///
/// Returns a with the and
/// . The same instance is returned by every subsequent call.
///
public static GUIContent TempContent(SerializedProperty property, bool narrowText = true)
{
return TempContent(property.displayName, property.tooltip, narrowText);
}
/************************************************************************************************************************/
private static ConversionCache _F1Cache;
private static GUIStyle _WeightLabelStyle;
private static float _WeightValueWidth;
///
/// Draws a label showing the `weight` aligned to the right side of the `area` and reduces its
/// to remove that label from its area.
///
public static void DoWeightLabel(ref Rect area, float weight)
{
if (_F1Cache == null)
{
_F1Cache = new ConversionCache((value) => value.ToString("F1"));
_WeightLabelStyle = new GUIStyle(GUI.skin.label);
_WeightValueWidth = _WeightLabelStyle.CalculateWidth("0.0");
}
var weightArea = StealFromRight(ref area, _WeightValueWidth);
var label = _F1Cache.Convert(weight);
_WeightLabelStyle.normal.textColor = Color.Lerp(Color.grey, TextColor, weight);
_WeightLabelStyle.fontStyle = Mathf.Approximately(weight * 10, (int)(weight * 10)) ?
FontStyle.Normal : FontStyle.Italic;
GUI.Label(weightArea, label, _WeightLabelStyle);
}
/************************************************************************************************************************/
/// The from before .
private static float _TightLabelWidth;
/// Stores the and changes it to the exact width of the `label`.
public static string BeginTightLabel(string label)
{
_TightLabelWidth = EditorGUIUtility.labelWidth;
EditorGUIUtility.labelWidth = CalculateLabelWidth(label) + EditorGUI.indentLevel * IndentSize;
return GetNarrowText(label);
}
/// Reverts to its previous value.
public static void EndTightLabel()
{
EditorGUIUtility.labelWidth = _TightLabelWidth;
}
/************************************************************************************************************************/
private static ConversionCache _NarrowTextCache;
///
/// Returns the `text` without any spaces if is false.
/// Otherwise simply returns the `text` without any changes.
///
public static string GetNarrowText(string text)
{
if (EditorGUIUtility.wideMode ||
string.IsNullOrEmpty(text))
return text;
if (_NarrowTextCache == null)
_NarrowTextCache = new ConversionCache((str) => str.Replace(" ", ""));
return _NarrowTextCache.Convert(text);
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Events
/************************************************************************************************************************/
///
/// Returns true and uses the current event if it is inside the specified
/// `area`.
///
public static bool TryUseClickEvent(Rect area, int button = -1)
{
var currentEvent = Event.current;
if (currentEvent.type == EventType.MouseUp &&
(button < 0 || currentEvent.button == button) &&
area.Contains(currentEvent.mousePosition))
{
GUI.changed = true;
currentEvent.Use();
return true;
}
else return false;
}
///
/// Returns true and uses the current event if it is inside the last GUI Layout
/// that was drawn.
///
public static bool TryUseClickEventInLastRect(int button = -1)
{
return TryUseClickEvent(GUILayoutUtility.GetLastRect(), button);
}
/************************************************************************************************************************/
///
/// Invokes `onDrop` if the is a drag and drop event inside the `dropArea`.
///
public static void HandleDragAndDrop(Rect dropArea, Func validate, Action onDrop) where T : class
{
if (!dropArea.Contains(Event.current.mousePosition))
return;
bool isDrop;
switch (Event.current.type)
{
case EventType.DragUpdated:
isDrop = false;
break;
case EventType.DragPerform:
isDrop = true;
break;
default:
return;
}
var dragging = DragAndDrop.objectReferences;
TryDrop(dragging, validate, onDrop, isDrop);
}
/************************************************************************************************************************/
///
/// Updates the of calls `onDrop` for each of the `objects`.
///
private static void TryDrop(IEnumerable objects, Func validate, Action onDrop, bool isDrop) where T : class
{
if (objects == null)
return;
var droppedAny = false;
foreach (var obj in objects)
{
var t = obj as T;
if (t != null && (validate == null || validate(t)))
{
if (!isDrop)
{
DragAndDrop.visualMode = DragAndDropVisualMode.Copy;
break;
}
else
{
onDrop(t);
droppedAny = true;
}
}
}
if (droppedAny)
GUIUtility.ExitGUI();
}
/************************************************************************************************************************/
///
/// Uses to deal with drag and drop operations involving
/// s of s.
///
public static void HandleDragAndDropAnimations(Rect dropArea, Action onDrop)
{
HandleDragAndDrop(dropArea, (clip) => !clip.legacy, onDrop);
HandleDragAndDrop(dropArea, null, (source) =>
{
var clips = ObjectPool.AcquireList();
source.GetAnimationClips(clips);
TryDrop(clips, (clip) => !clip.legacy, onDrop, true);
ObjectPool.Release(clips);
});
}
/************************************************************************************************************************/
/// Deselects any selected IMGUI control.
public static void Deselect()
{
GUIUtility.keyboardControl = 0;
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Fields
/************************************************************************************************************************/
///
/// Draw a with an alternate cached string when it
/// is not selected (for example, "1" might become "1s" to indicate "seconds").
///
public static float DoSpecialFloatField(Rect area, GUIContent label, float value, ConversionCache toString)
{
// Treat most events normally, but when repainting show a text field with the cached string.
if (label != null)
{
if (Event.current.type != EventType.Repaint)
return EditorGUI.FloatField(area, label, value);
var dragArea = new Rect(area.x, area.y, EditorGUIUtility.labelWidth, area.height);
EditorGUIUtility.AddCursorRect(dragArea, MouseCursor.SlideArrow);
EditorGUI.TextField(area, label, toString.Convert(value));
}
else
{
var indentLevel = EditorGUI.indentLevel;
EditorGUI.indentLevel = 0;
if (Event.current.type != EventType.Repaint)
value = EditorGUI.FloatField(area, value);
else
EditorGUI.TextField(area, toString.Convert(value));
EditorGUI.indentLevel = indentLevel;
}
return value;
}
///
/// Draw a with an alternate cached string when it
/// is not selected (for example, "1" might become "1s" to indicate "seconds").
///
public static void DoFloatFieldWithSuffix(Rect area, GUIContent label, SerializedProperty property,
ConversionCache toString)
{
label = EditorGUI.BeginProperty(area, label, property);
EditorGUI.BeginChangeCheck();
var value = DoSpecialFloatField(area, label, property.floatValue, toString);
if (EditorGUI.EndChangeCheck())
property.floatValue = value;
EditorGUI.EndProperty();
}
/************************************************************************************************************************/
///
/// Draw a which sets the value to
/// when disabled followed by two float fields to display the as
/// both normalized time and seconds.
///
public static void DoOptionalTimeField(ref Rect area, GUIContent label, SerializedProperty property,
bool timeIsNormalized, float length, float defaultValue = 0, bool isOptional = true)
{
label = EditorGUI.BeginProperty(area, label, property);
EditorGUI.BeginChangeCheck();
var value = DoOptionalTimeField(ref area, label, property.floatValue, timeIsNormalized,
length, defaultValue, isOptional);
if (EditorGUI.EndChangeCheck())
property.floatValue = value;
EditorGUI.EndProperty();
}
/************************************************************************************************************************/
private static ConversionCache _XSuffixCache, _SSuffixCache;
///
/// Draw a which sets the value to
/// when disabled followed by two float fields to display the `time` both normalized and in seconds.
///
public static float DoOptionalTimeField(ref Rect area, GUIContent label, float time, bool timeIsNormalized,
float length, float defaultValue = 0, bool isOptional = true)
{
if (_XSuffixCache == null)
{
_XSuffixCache = new ConversionCache((x) => x + "x");
_SSuffixCache = new ConversionCache((s) => s + "s");
}
area.height = LineHeight;
bool showNormalized, showSeconds;
if (length > 0)
{
showNormalized = showSeconds = true;
}
else
{
showNormalized = timeIsNormalized;
showSeconds = !timeIsNormalized;
}
var labelWidth = EditorGUIUtility.labelWidth;
var enabled = GUI.enabled;
var toggleArea = area;
if (isOptional)
{
toggleArea.x += EditorGUIUtility.labelWidth;
toggleArea.width = ToggleWidth;
EditorGUIUtility.labelWidth += toggleArea.width;
EditorGUIUtility.AddCursorRect(toggleArea, MouseCursor.Arrow);
// We need to draw the toggle after everything else to it goes on top of the label. But we want it to
// get priority for input events, so we disable the other controls during those events in its area.
var currentEvent = Event.current;
if (enabled && toggleArea.Contains(currentEvent.mousePosition))
{
switch (currentEvent.type)
{
case EventType.Repaint:
case EventType.Layout:
break;
default:
GUI.enabled = false;
break;
}
}
}
else if (float.IsNaN(time))
{
time = defaultValue;
}
var displayTime = float.IsNaN(time) ? defaultValue : time;
var normalizedArea = area;
var secondsArea = area;
if (showNormalized)
{
if (showSeconds)
{
var split = (EditorGUIUtility.labelWidth + normalizedArea.xMax - StandardSpacing) * 0.5f;
normalizedArea.xMax = split;
secondsArea.xMin = split + StandardSpacing;
}
var normalizedTime = timeIsNormalized ? displayTime : displayTime / length;
EditorGUI.BeginChangeCheck();
normalizedTime = DoSpecialFloatField(normalizedArea, label, normalizedTime, _XSuffixCache);
if (EditorGUI.EndChangeCheck())
time = timeIsNormalized ? normalizedTime : normalizedTime * length;
}
EditorGUIUtility.labelWidth = labelWidth;
if (showSeconds)
{
var rawTime = timeIsNormalized ? displayTime * length : displayTime;
if (showNormalized)
label = null;
EditorGUI.BeginChangeCheck();
rawTime = DoSpecialFloatField(secondsArea, label, rawTime, _SSuffixCache);
if (EditorGUI.EndChangeCheck())
{
if (timeIsNormalized)
{
if (length != 0)
time = rawTime / length;
}
else
{
time = rawTime;
}
}
}
GUI.enabled = enabled;
if (isOptional)
DoOptionalTimeToggle(toggleArea, ref time, defaultValue);
return time;
}
/************************************************************************************************************************/
private static void DoOptionalTimeToggle(Rect area, ref float time, float defaultValue)
{
#if UNITY_2019_3_OR_NEWER
area.x += 2;
#endif
var wasEnabled = !float.IsNaN(time);
var isEnabled = GUI.Toggle(area, wasEnabled, GUIContent.none);
if (isEnabled != wasEnabled)
{
time = isEnabled ? defaultValue : float.NaN;
Deselect();
}
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
}
}
#endif