1071 lines
44 KiB
C#
1071 lines
44 KiB
C#
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 Kybernetik //
|
|
|
|
#if UNITY_EDITOR
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using UnityEditor;
|
|
using UnityEngine;
|
|
using Object = UnityEngine.Object;
|
|
|
|
namespace Animancer.Editor
|
|
{
|
|
/// <summary>[Editor-Only] Various GUI utilities used throughout Animancer.</summary>
|
|
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/AnimancerGUI
|
|
public static partial class AnimancerGUI
|
|
{
|
|
/************************************************************************************************************************/
|
|
#region Standard Values
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>The highlight color used for fields showing a warning.</summary>
|
|
public static readonly Color
|
|
WarningFieldColor = new(1, 0.9f, 0.6f);
|
|
|
|
/// <summary>The highlight color used for fields showing an error.</summary>
|
|
public static readonly Color
|
|
ErrorFieldColor = new(1, 0.6f, 0.6f);
|
|
|
|
/// <summary>Returns a color with uniform Red, Green, and Blue values.</summary>
|
|
public static Color Grey(float rgb, float alpha = 1)
|
|
=> new(rgb, rgb, rgb, alpha);
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary><see cref="GUILayout.ExpandWidth"/> set to false.</summary>
|
|
public static readonly GUILayoutOption[]
|
|
DontExpandWidth = { GUILayout.ExpandWidth(false) };
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>Returns <see cref="EditorGUIUtility.singleLineHeight"/>.</summary>
|
|
public static float LineHeight => EditorGUIUtility.singleLineHeight;
|
|
|
|
/// <summary>
|
|
/// Calculates the number of vertical pixels required to draw the specified `lineCount` using the
|
|
/// <see cref="LineHeight"/> and <see cref="StandardSpacing"/>.
|
|
/// </summary>
|
|
public static float CalculateHeight(int lineCount)
|
|
=> lineCount <= 0
|
|
? 0
|
|
: LineHeight * lineCount + StandardSpacing * (lineCount - 1);
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>Returns <see cref="EditorGUIUtility.standardVerticalSpacing"/>.</summary>
|
|
public static float StandardSpacing => EditorGUIUtility.standardVerticalSpacing;
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
private static float _IndentSize = float.NaN;
|
|
|
|
/// <summary>
|
|
/// The number of pixels of indentation for each <see cref="EditorGUI.indentLevel"/> increment.
|
|
/// </summary>
|
|
public static float IndentSize
|
|
{
|
|
get
|
|
{
|
|
if (float.IsNaN(_IndentSize))
|
|
{
|
|
var indentLevel = EditorGUI.indentLevel;
|
|
EditorGUI.indentLevel = 1;
|
|
_IndentSize = EditorGUI.IndentedRect(default).x;
|
|
EditorGUI.indentLevel = indentLevel;
|
|
}
|
|
|
|
return _IndentSize;
|
|
}
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
private static float _ToggleWidth = -1;
|
|
|
|
/// <summary>The width of a standard <see cref="GUISkin.toggle"/> with no label.</summary>
|
|
public static float ToggleWidth
|
|
{
|
|
get
|
|
{
|
|
if (_ToggleWidth == -1)
|
|
_ToggleWidth = GUI.skin.toggle.CalculateWidth(GUIContent.none);
|
|
return _ToggleWidth;
|
|
}
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>The color of the standard label text.</summary>
|
|
public static Color TextColor
|
|
=> GUI.skin.label.normal.textColor;
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
private static GUIStyle _MiniButtonStyle;
|
|
|
|
/// <summary>A more compact <see cref="EditorStyles.miniButton"/> with a fixed size as a tiny box.</summary>
|
|
public static GUIStyle MiniButtonStyle
|
|
=> _MiniButtonStyle ??= new(EditorStyles.miniButton)
|
|
{
|
|
margin = new(0, 0, 2, 0),
|
|
padding = new(2, 3, 2, 2),
|
|
alignment = TextAnchor.MiddleCenter,
|
|
fixedHeight = LineHeight,
|
|
fixedWidth = LineHeight - 1,
|
|
};
|
|
|
|
private static GUIStyle _NoPaddingButtonStyle;
|
|
|
|
/// <summary><see cref="MiniButtonStyle"/> with no <see cref="GUIStyle.padding"/>.</summary>
|
|
public static GUIStyle NoPaddingButtonStyle
|
|
=> _NoPaddingButtonStyle ??= new(MiniButtonStyle)
|
|
{
|
|
padding = new(),
|
|
fixedWidth = LineHeight,
|
|
};
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
private static GUIStyle _RightLabelStyle;
|
|
|
|
/// <summary><see cref="EditorStyles.label"/> using <see cref="TextAnchor.MiddleRight"/>.</summary>
|
|
public static GUIStyle RightLabelStyle
|
|
=> _RightLabelStyle ??= new(EditorStyles.label)
|
|
{
|
|
alignment = TextAnchor.MiddleRight,
|
|
};
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
private static GUIStyle _MiniButtonNoPadding;
|
|
|
|
/// <summary>A more compact <see cref="EditorStyles.miniButton"/> with no padding for its content.</summary>
|
|
public static GUIStyle MiniButtonNoPadding
|
|
{
|
|
get
|
|
{
|
|
_MiniButtonNoPadding ??= new(EditorStyles.miniButton)
|
|
{
|
|
padding = new(),
|
|
overflow = new(),
|
|
};
|
|
|
|
return _MiniButtonNoPadding;
|
|
}
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>Constants used by <see cref="Event.commandName"/>.</summary>
|
|
/// <remarks>Key combinations are listed for Windows. Other platforms may differ.</remarks>
|
|
public static class Commands
|
|
{
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary><see cref="KeyCode.Delete"/></summary>
|
|
public const string SoftDelete = "SoftDelete";
|
|
|
|
/// <summary><see cref="KeyCode.LeftControl"/> + <see cref="KeyCode.Delete"/></summary>
|
|
public const string Delete = "Delete";
|
|
|
|
/// <summary><see cref="KeyCode.LeftControl"/> + <see cref="KeyCode.C"/></summary>
|
|
public const string Copy = "Copy";
|
|
|
|
/// <summary><see cref="KeyCode.LeftControl"/> + <see cref="KeyCode.X"/></summary>
|
|
public const string Cut = "Cut";
|
|
|
|
/// <summary><see cref="KeyCode.LeftControl"/> + <see cref="KeyCode.V"/></summary>
|
|
public const string Paste = "Paste";
|
|
|
|
/// <summary><see cref="KeyCode.LeftControl"/> + <see cref="KeyCode.D"/></summary>
|
|
public const string Duplicate = "Duplicate";
|
|
|
|
/// <summary><see cref="KeyCode.LeftControl"/> + <see cref="KeyCode.A"/></summary>
|
|
public const string SelectAll = "SelectAll";
|
|
|
|
/// <summary><see cref="KeyCode.F"/></summary>
|
|
public const string FrameSelected = "FrameSelected";
|
|
|
|
/// <summary><see cref="KeyCode.LeftShift"/> + <see cref="KeyCode.F"/></summary>
|
|
public const string FrameSelectedWithLock = "FrameSelectedWithLock";
|
|
|
|
/// <summary><see cref="KeyCode.LeftControl"/> + <see cref="KeyCode.F"/></summary>
|
|
public const string Find = "Find";
|
|
|
|
/************************************************************************************************************************/
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
#endregion
|
|
/************************************************************************************************************************/
|
|
#region Layout
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>The offset currently applied to the GUI by <see cref="GUI.BeginGroup(Rect)"/>.</summary>
|
|
public static Vector2 GuiOffset { get; set; }
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>Calls <see cref="UnityEditorInternal.InternalEditorUtility.RepaintAllViews"/>.</summary>
|
|
public static void RepaintEverything()
|
|
=> UnityEditorInternal.InternalEditorUtility.RepaintAllViews();
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary><see cref="GUILayoutUtility.GetRect(float, float)"/></summary>
|
|
public static Rect LayoutRect(float height)
|
|
=> GUILayoutUtility.GetRect(0, height);
|
|
|
|
/// <summary><see cref="GUILayoutUtility.GetRect(float, float, GUIStyle)"/></summary>
|
|
public static Rect LayoutRect(float height, GUIStyle style)
|
|
=> GUILayoutUtility.GetRect(0, height, style);
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>Indicates where <see cref="LayoutSingleLineRect"/> should add the <see cref="StandardSpacing"/>.</summary>
|
|
public enum SpacingMode
|
|
{
|
|
/// <summary>No extra space.</summary>
|
|
None,
|
|
|
|
/// <summary>Add extra space before the new area.</summary>
|
|
Before,
|
|
|
|
/// <summary>Add extra space after the new area.</summary>
|
|
After,
|
|
|
|
/// <summary>Add extra space before and after the new area.</summary>
|
|
BeforeAndAfter
|
|
}
|
|
|
|
/// <summary>
|
|
/// Uses <see cref="GUILayoutUtility.GetRect(float, float)"/> to get a <see cref="Rect"/> with the specified
|
|
/// `height` and the <see cref="StandardSpacing"/> added according to the specified `spacing`.
|
|
/// </summary>
|
|
public static Rect LayoutRect(float height, SpacingMode spacing)
|
|
{
|
|
Rect rect;
|
|
switch (spacing)
|
|
{
|
|
case SpacingMode.None:
|
|
return LayoutRect(height);
|
|
|
|
case SpacingMode.Before:
|
|
rect = LayoutRect(height + StandardSpacing);
|
|
rect.yMin += StandardSpacing;
|
|
return rect;
|
|
|
|
case SpacingMode.After:
|
|
rect = LayoutRect(height + StandardSpacing);
|
|
rect.height -= StandardSpacing;
|
|
return rect;
|
|
|
|
case SpacingMode.BeforeAndAfter:
|
|
rect = LayoutRect(height + StandardSpacing * 2);
|
|
rect.yMin += StandardSpacing;
|
|
rect.height -= StandardSpacing;
|
|
return rect;
|
|
|
|
default:
|
|
throw new ArgumentException($"Unsupported {nameof(StandardSpacing)}: " + spacing, nameof(spacing));
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Uses <see cref="GUILayoutUtility.GetRect(float, float)"/> to get a <see cref="Rect"/> occupying a single
|
|
/// standard line with the <see cref="StandardSpacing"/> added according to the specified `spacing`.
|
|
/// </summary>
|
|
public static Rect LayoutSingleLineRect(SpacingMode spacing = SpacingMode.None)
|
|
=> LayoutRect(LineHeight, spacing);
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>
|
|
/// If the <see cref="Rect.height"/> is positive, this method moves the <see cref="Rect.y"/> by that amount and
|
|
/// adds the <see cref="StandardSpacing"/>.
|
|
/// </summary>
|
|
public static void NextVerticalArea(ref Rect area)
|
|
{
|
|
if (area.height > 0)
|
|
area.y += area.height + StandardSpacing;
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>
|
|
/// Subtracts the `width` from the left side of the `area`
|
|
/// and returns a new <see cref="Rect"/> occupying the removed section.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Subtracts the `width` from the right side of the `area`
|
|
/// and returns a new <see cref="Rect"/> occupying the removed section.
|
|
/// </summary>
|
|
public static Rect StealFromRight(ref Rect area, float width, float padding = 0)
|
|
{
|
|
area.width -= width + padding;
|
|
return new(area.xMax + padding, area.y, width, area.height);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Subtracts the `height` from the top side of the `area`
|
|
/// and returns a new <see cref="Rect"/> occupying the removed section.
|
|
/// </summary>
|
|
public static Rect StealFromTop(ref Rect area, float height, float padding = 0)
|
|
{
|
|
var newRect = new Rect(area.x, area.y, area.width, height);
|
|
area.yMin += height + padding;
|
|
return newRect;
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>
|
|
/// Subtracts the <see cref="LineHeight"/> from the top side of the `area`
|
|
/// and returns a new <see cref="Rect"/> occupying the removed section.
|
|
/// </summary>
|
|
public static Rect StealLineFromTop(ref Rect area)
|
|
=> StealFromTop(ref area, LineHeight, StandardSpacing);
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>
|
|
/// Returns a copy of the `rect` expanded by the specified `amount`
|
|
/// (or contracted if negative).
|
|
/// </summary>
|
|
public static Rect Expand(this Rect rect, float amount)
|
|
=> new(
|
|
rect.x - amount,
|
|
rect.y - amount,
|
|
rect.width + amount * 2,
|
|
rect.height + amount * 2);
|
|
|
|
/// <summary>
|
|
/// Returns a copy of the `rect` expanded by the specified amounts
|
|
/// on each axis (or contracted if negative).
|
|
/// </summary>
|
|
public static Rect Expand(this Rect rect, float x, float y)
|
|
=> new(
|
|
rect.x - x,
|
|
rect.y - y,
|
|
rect.width + x * 2,
|
|
rect.height + y * 2);
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>Returns a copy of the `rect` expanded to include the `other`.</summary>
|
|
public static Rect Encapsulate(this Rect rect, Rect other)
|
|
=> Rect.MinMaxRect(
|
|
Math.Min(rect.xMin, other.xMin),
|
|
Math.Min(rect.yMin, other.yMin),
|
|
Math.Max(rect.xMax, other.xMax),
|
|
Math.Max(rect.yMax, other.yMax));
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>
|
|
/// Divides the given `area` such that the fields associated with both labels will have equal space
|
|
/// remaining after the labels themselves.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>[Animancer Extension] Calls <see cref="GUIStyle.CalcMinMaxWidth"/> and returns the max width.</summary>
|
|
public static float CalculateWidth(this GUIStyle style, GUIContent content)
|
|
{
|
|
style.CalcMinMaxWidth(content, out _, out var width);
|
|
return Mathf.Ceil(width);
|
|
}
|
|
|
|
/// <summary>[Animancer Extension] Calls <see cref="GUIStyle.CalcMinMaxWidth"/> and returns the max width.</summary>
|
|
public static float CalculateWidth(this GUIStyle style, string text)
|
|
{
|
|
using (var content = PooledGUIContent.Acquire(text))
|
|
return style.CalculateWidth(content);
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
private static ConversionCache<string, float> _LabelWidthCache;
|
|
|
|
/// <summary>
|
|
/// Calls <see cref="GUIStyle.CalcMinMaxWidth"/> using <see cref="GUISkin.label"/> and returns the max
|
|
/// width. The result is cached for efficient reuse.
|
|
/// </summary>
|
|
public static float CalculateLabelWidth(string text)
|
|
{
|
|
_LabelWidthCache ??= ConversionCache.CreateWidthCache(GUI.skin.label);
|
|
|
|
return _LabelWidthCache.Convert(text);
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
private static string[] _IntToStringCache;
|
|
|
|
/// <summary>Caches and returns <see cref="int.ToString()"/> if <c>0 <= value < 100</c>.</summary>
|
|
public static string ToStringCached(this int value)
|
|
{
|
|
const int CacheSize = 100;
|
|
|
|
if (value < 0 || value >= CacheSize)
|
|
return value.ToString();
|
|
|
|
if (_IntToStringCache == null)
|
|
{
|
|
_IntToStringCache = new string[CacheSize];
|
|
for (int i = 0; i < _IntToStringCache.Length; i++)
|
|
_IntToStringCache[i] = i.ToString();
|
|
}
|
|
|
|
return _IntToStringCache[value];
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>
|
|
/// Begins a vertical layout group using the given style and decreases the
|
|
/// <see cref="EditorGUIUtility.labelWidth"/> to compensate for the indentation.
|
|
/// </summary>
|
|
public static void BeginVerticalBox(GUIStyle style)
|
|
{
|
|
if (style == null)
|
|
{
|
|
GUILayout.BeginVertical();
|
|
return;
|
|
}
|
|
|
|
GUILayout.BeginVertical(style);
|
|
EditorGUIUtility.labelWidth -= style.padding.left;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Ends a layout group started by <see cref="BeginVerticalBox"/> and restores the
|
|
/// <see cref="EditorGUIUtility.labelWidth"/>.
|
|
/// </summary>
|
|
public static void EndVerticalBox(GUIStyle style)
|
|
{
|
|
if (style != null)
|
|
EditorGUIUtility.labelWidth += style.padding.left;
|
|
|
|
GUILayout.EndVertical();
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
private static Func<Rect> _GetGUIClipRect;
|
|
|
|
/// <summary>Returns the <see cref="Rect"/> of the current <see cref="GUI.BeginClip(Rect)"/>.</summary>
|
|
public static Rect GetGUIClipRect()
|
|
{
|
|
if (_GetGUIClipRect != null)
|
|
return _GetGUIClipRect();
|
|
|
|
var type = typeof(GUI).Assembly.GetType("UnityEngine.GUIClip");
|
|
var method = type?.GetMethod("GetTopRect", AnimancerReflection.AnyAccessBindings);
|
|
|
|
if (method != null &&
|
|
method.ReturnType != null &&
|
|
method.GetParameters().Length == 0)
|
|
{
|
|
_GetGUIClipRect = (Func<Rect>)Delegate.CreateDelegate(typeof(Func<Rect>), method);
|
|
}
|
|
else
|
|
{
|
|
_GetGUIClipRect = () => new(0, 0, Screen.width, Screen.height);
|
|
}
|
|
|
|
return _GetGUIClipRect();
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
#endregion
|
|
/************************************************************************************************************************/
|
|
#region Labels
|
|
/************************************************************************************************************************/
|
|
|
|
private static GUIStyle _WeightLabelStyle;
|
|
private static float _WeightLabelWidth = -1;
|
|
|
|
/// <summary>
|
|
/// Draws a label showing the `weight` aligned to the right side of the `area` and reduces its
|
|
/// <see cref="Rect.width"/> to remove that label from its area.
|
|
/// </summary>
|
|
public static void DoWeightLabel(ref Rect area, float weight, float effectiveWeight)
|
|
{
|
|
var label = WeightToShortString(weight, out var isExact);
|
|
|
|
_WeightLabelStyle ??= new(GUI.skin.label)
|
|
{
|
|
alignment = TextAnchor.MiddleRight,
|
|
};
|
|
|
|
if (_WeightLabelWidth < 0)
|
|
{
|
|
_WeightLabelStyle.fontStyle = FontStyle.Italic;
|
|
_WeightLabelWidth = _WeightLabelStyle.CalculateWidth("0.0");
|
|
}
|
|
|
|
_WeightLabelStyle.normal.textColor = Color.Lerp(Color.grey, TextColor, 0.2f + effectiveWeight * 0.8f);
|
|
_WeightLabelStyle.fontStyle = isExact ? FontStyle.Normal : FontStyle.Italic;
|
|
|
|
var weightArea = StealFromRight(ref area, _WeightLabelWidth);
|
|
|
|
GUI.Label(weightArea, label, _WeightLabelStyle);
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
private static ConversionCache<float, string> _ShortWeightCache;
|
|
|
|
/// <summary>Returns a string which approximates the `weight` into no more than 3 digits.</summary>
|
|
private static string WeightToShortString(float weight, out bool isExact)
|
|
{
|
|
isExact = true;
|
|
|
|
if (weight == 0)
|
|
return "0.0";
|
|
if (weight == 1)
|
|
return "1.0";
|
|
|
|
isExact = false;
|
|
|
|
if (weight >= -0.5f && weight < 0.05f)
|
|
return "~0.";
|
|
if (weight >= 0.95f && weight < 1.05f)
|
|
return "~1.";
|
|
|
|
if (weight <= -99.5f)
|
|
return "-??";
|
|
if (weight >= 999.5f)
|
|
return "???";
|
|
|
|
_ShortWeightCache ??= new(value =>
|
|
{
|
|
if (value < -9.5f) return $"{value:F0}";
|
|
if (value < -0.5f) return $"{value:F0}.";
|
|
if (value < 9.5f) return $"{value:F1}";
|
|
if (value < 99.5f) return $"{value:F0}.";
|
|
return $"{value:F0}";
|
|
});
|
|
|
|
var rounded = weight > 0 ? Mathf.Floor(weight * 10) : Mathf.Ceil(weight * 10);
|
|
isExact = Mathf.Approximately(weight * 10, rounded);
|
|
|
|
return _ShortWeightCache.Convert(weight);
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>The <see cref="EditorGUIUtility.labelWidth"/> from before <see cref="BeginTightLabel"/>.</summary>
|
|
private static float _TightLabelWidth;
|
|
|
|
/// <summary>
|
|
/// Stores the <see cref="EditorGUIUtility.labelWidth"/> and changes it to the exact width of the `label`.
|
|
/// </summary>
|
|
public static string BeginTightLabel(string label)
|
|
{
|
|
_TightLabelWidth = EditorGUIUtility.labelWidth;
|
|
EditorGUIUtility.labelWidth = CalculateLabelWidth(label) + EditorGUI.indentLevel * IndentSize;
|
|
return label;
|
|
}
|
|
|
|
/// <summary>Reverts <see cref="EditorGUIUtility.labelWidth"/> to its previous value.</summary>
|
|
public static void EndTightLabel()
|
|
{
|
|
EditorGUIUtility.labelWidth = _TightLabelWidth;
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>Draws a button using <see cref="EditorStyles.miniButton"/> and <see cref="DontExpandWidth"/>.</summary>
|
|
public static bool CompactMiniButton(GUIContent content)
|
|
=> GUILayout.Button(content, EditorStyles.miniButton, DontExpandWidth);
|
|
|
|
/// <summary>Draws a button using <see cref="EditorStyles.miniButton"/>.</summary>
|
|
public static bool CompactMiniButton(Rect area, GUIContent content)
|
|
=> GUI.Button(area, content, EditorStyles.miniButton);
|
|
|
|
/************************************************************************************************************************/
|
|
#endregion
|
|
/************************************************************************************************************************/
|
|
#region Fields
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>Draws a label field with a foldout.</summary>
|
|
public static bool DoLabelFoldoutFieldGUI(string label, string value, bool isExpanded)
|
|
{
|
|
using (var labelContent = PooledGUIContent.Acquire(label))
|
|
using (var valueContent = PooledGUIContent.Acquire(value))
|
|
return DoLabelFoldoutFieldGUI(labelContent, valueContent, isExpanded);
|
|
}
|
|
|
|
/// <summary>Draws a label field with a foldout.</summary>
|
|
public static bool DoLabelFoldoutFieldGUI(GUIContent label, GUIContent value, bool isExpanded)
|
|
{
|
|
var area = LayoutSingleLineRect();
|
|
|
|
EditorGUI.LabelField(area, label, value);
|
|
|
|
return EditorGUI.Foldout(area, isExpanded, "", true);
|
|
}
|
|
|
|
/// <summary>Draws a foldout which stores its state in a hash set.</summary>
|
|
public static bool DoHashedFoldoutGUI<T>(Rect area, HashSet<T> expandedItems, T item)
|
|
{
|
|
var wasExpanded = expandedItems.Contains(item);
|
|
var isExpanded = EditorGUI.Foldout(area, wasExpanded, "", true);
|
|
|
|
if (isExpanded != wasExpanded)
|
|
if (isExpanded)
|
|
expandedItems.Add(item);
|
|
else
|
|
expandedItems.Remove(item);
|
|
|
|
return isExpanded;
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>Draws an object reference field.</summary>
|
|
public static T DoObjectFieldGUI<T>(
|
|
Rect area,
|
|
GUIContent label,
|
|
T value,
|
|
bool allowSceneObjects)
|
|
where T : Object
|
|
=> EditorGUI.ObjectField(area, label, value, typeof(T), allowSceneObjects) as T;
|
|
|
|
/// <summary>Draws an object reference field.</summary>
|
|
public static T DoObjectFieldGUI<T>(
|
|
Rect area,
|
|
string label,
|
|
T value,
|
|
bool allowSceneObjects)
|
|
where T : Object
|
|
{
|
|
using var content = PooledGUIContent.Acquire(label);
|
|
return DoObjectFieldGUI(area, content, value, allowSceneObjects);
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>Draws an object reference field.</summary>
|
|
public static T DoObjectFieldGUI<T>(
|
|
GUIContent label,
|
|
T value,
|
|
bool allowSceneObjects)
|
|
where T : Object
|
|
{
|
|
var height = EditorGUIUtility.HasObjectThumbnail(typeof(T)) ? 64f : LineHeight;
|
|
var area = EditorGUILayout.GetControlRect(label != null, height);
|
|
return DoObjectFieldGUI(area, label, value, allowSceneObjects);
|
|
}
|
|
|
|
/// <summary>Draws an object reference field.</summary>
|
|
public static T DoObjectFieldGUI<T>(
|
|
string label,
|
|
T value,
|
|
bool allowSceneObjects)
|
|
where T : Object
|
|
{
|
|
using var content = PooledGUIContent.Acquire(label);
|
|
return DoObjectFieldGUI(content, value, allowSceneObjects);
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>
|
|
/// Draws an object reference field with a dropdown button as its label
|
|
/// and returns true if clicked.
|
|
/// </summary>
|
|
public static bool DoDropdownObjectFieldGUI<T>(
|
|
Rect area,
|
|
GUIContent label,
|
|
bool showDropdown,
|
|
ref T value)
|
|
where T : Object
|
|
{
|
|
var labelWidth = EditorGUIUtility.labelWidth;
|
|
|
|
labelWidth += 2;
|
|
area.xMin -= 1;
|
|
|
|
var spacing = StandardSpacing;
|
|
var labelArea = StealFromLeft(ref area, labelWidth - spacing, spacing);
|
|
|
|
value = DoObjectFieldGUI(area, "", value, true);
|
|
|
|
if (showDropdown)
|
|
{
|
|
return EditorGUI.DropdownButton(labelArea, label, FocusType.Passive);
|
|
}
|
|
else
|
|
{
|
|
GUI.Label(labelArea, label);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
#endregion
|
|
/************************************************************************************************************************/
|
|
#region Events
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>Sets <see cref="GUI.changed"/> if `guiChanged` is <c>true</c>.</summary>
|
|
public static void SetGuiChanged(bool guiChanged)
|
|
{
|
|
if (guiChanged)
|
|
GUI.changed = true;
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>
|
|
/// Calls <see cref="Event.Use"/> and sets the
|
|
/// <see cref="GUI.changed"/> and <see cref="GUIUtility.hotControl"/>.
|
|
/// </summary>
|
|
public static void Use(this Event guiEvent, int controlId, bool guiChanged = false)
|
|
{
|
|
SetGuiChanged(guiChanged);
|
|
GUIUtility.hotControl = controlId;
|
|
guiEvent.Use();
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>
|
|
/// Sets the <see cref="GUIUtility.hotControl"/> and uses the `currentEvent`
|
|
/// if the mouse position is inside the `area`.
|
|
/// </summary>
|
|
/// <remarks>This method is useful for handling <see cref="EventType.MouseDown"/>.</remarks>
|
|
public static bool TryUseMouseDown(Rect area, Event currentEvent, int controlID)
|
|
{
|
|
if (!area.Contains(currentEvent.mousePosition))
|
|
return false;
|
|
|
|
GUIUtility.keyboardControl = 0;
|
|
GUIUtility.hotControl = controlID;
|
|
currentEvent.Use();
|
|
return true;
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>
|
|
/// Releases the <see cref="GUIUtility.hotControl"/> and uses the `currentEvent` if it was the active control.
|
|
/// </summary>
|
|
/// <remarks>This method is useful for handling <see cref="EventType.MouseUp"/>.</remarks>
|
|
public static bool TryUseMouseUp(Event currentEvent, int controlID, bool guiChanged = false)
|
|
{
|
|
if (GUIUtility.hotControl != controlID)
|
|
return false;
|
|
|
|
GUIUtility.hotControl = 0;
|
|
currentEvent.Use();
|
|
SetGuiChanged(guiChanged);
|
|
return true;
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>
|
|
/// Uses the `currentEvent` and sets <see cref="GUI.changed"/>
|
|
/// if the `controlID` matches the <see cref="GUIUtility.hotControl"/>.
|
|
/// </summary>
|
|
/// <remarks>This method is useful for handling <see cref="EventType.MouseDrag"/>.</remarks>
|
|
public static bool TryUseHotControl(Event currentEvent, int controlID, bool guiChanged = true)
|
|
{
|
|
if (GUIUtility.hotControl != controlID)
|
|
return false;
|
|
|
|
SetGuiChanged(guiChanged);
|
|
currentEvent.Use();
|
|
return true;
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>
|
|
/// Uses the `currentEvent` if the `controlID` has <see cref="GUIUtility.keyboardControl"/>.
|
|
/// If a `key` is specified, other keys will be ignored.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// This method is useful for handling
|
|
/// <see cref="EventType.KeyDown"/> and <see cref="EventType.KeyUp"/>.
|
|
/// </remarks>
|
|
public static bool TryUseKey(Event currentEvent, int controlID, KeyCode key = KeyCode.None)
|
|
{
|
|
if (GUIUtility.keyboardControl != controlID)
|
|
return false;
|
|
|
|
if (key != KeyCode.None && currentEvent.keyCode != key)
|
|
return false;
|
|
|
|
currentEvent.Use();
|
|
GUI.changed = true;
|
|
return true;
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>
|
|
/// Returns true and uses the current event if it is
|
|
/// <see cref="EventType.MouseUp"/> inside the specified `area`.
|
|
/// </summary>
|
|
/// <remarks>Uses <see cref="EventType.MouseDown"/> and <see cref="EventType.MouseUp"/> events.</remarks>
|
|
public static bool TryUseClickEvent(Rect area, int button = -1, int controlID = 0)
|
|
{
|
|
if (controlID == 0)
|
|
controlID = GUIUtility.GetControlID(FocusType.Passive);
|
|
|
|
var currentEvent = Event.current;
|
|
|
|
if (button >= 0 && currentEvent.button != button)
|
|
return false;
|
|
|
|
switch (currentEvent.type)
|
|
{
|
|
case EventType.MouseDown:
|
|
TryUseMouseDown(area, currentEvent, controlID);
|
|
break;
|
|
|
|
case EventType.MouseUp:
|
|
return TryUseMouseUp(currentEvent, controlID, true) && area.Contains(currentEvent.mousePosition);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns true and uses the current event if it is <see cref="EventType.MouseUp"/> inside the last GUI Layout
|
|
/// <see cref="Rect"/> that was drawn.
|
|
/// </summary>
|
|
public static bool TryUseClickEventInLastRect(int button = -1)
|
|
=> TryUseClickEvent(GUILayoutUtility.GetLastRect(), button);
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>Is the `currentEvent` a Middle Click or Alt + Left Click? </summary>
|
|
public static bool IsMiddleClick(this Event currentEvent)
|
|
=> currentEvent.button == 2
|
|
|| (currentEvent.button == 0 && currentEvent.modifiers == EventModifiers.Alt);
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>Deselects any selected IMGUI control.</summary>
|
|
public static void Deselect()
|
|
{
|
|
GUIUtility.hotControl = 0;
|
|
GUIUtility.keyboardControl = 0;
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
#endregion
|
|
/************************************************************************************************************************/
|
|
#region Other
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>Draws a line.</summary>
|
|
/// <remarks>
|
|
/// Use <see cref="BeginTriangles"/>, <see cref="DrawLineBatched"/>, and <see cref="EndTriangles"/>
|
|
/// if you want to draw multiple lines more efficiently.
|
|
/// </remarks>
|
|
public static void DrawLine(
|
|
Vector2 a,
|
|
Vector2 b,
|
|
float width,
|
|
Color color)
|
|
{
|
|
BeginTriangles(color);
|
|
DrawLineBatched(a, b, width);
|
|
EndTriangles();
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>Sets up the rendering details for <see cref="DrawLineBatched"/>.</summary>
|
|
/// <remarks>
|
|
/// If the color doesn't work correctly, you may need to call
|
|
/// <see cref="Handles.DrawLine(Vector3, Vector3)"/> before this.
|
|
/// </remarks>
|
|
public static void BeginTriangles(Color color)
|
|
{
|
|
GL.Begin(GL.TRIANGLES);
|
|
|
|
GL.Color(color);
|
|
}
|
|
|
|
/// <summary>Cleans up the rendering details for <see cref="DrawLineBatched"/>.</summary>
|
|
public static void EndTriangles()
|
|
{
|
|
GL.End();
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>Draws a line.</summary>
|
|
/// <remarks>Must be called after <see cref="BeginTriangles"/> and before <see cref="EndTriangles"/>.</remarks>
|
|
public static void DrawLineBatched(
|
|
Vector2 a,
|
|
Vector2 b,
|
|
float width)
|
|
{
|
|
var perpendicular = 0.5f * width * (a - b).GetPerpendicular().normalized;
|
|
|
|
var a0 = a - perpendicular;
|
|
var a1 = a + perpendicular;
|
|
var b0 = b - perpendicular;
|
|
var b1 = b + perpendicular;
|
|
|
|
GL.Vertex(a0);
|
|
GL.Vertex(a1);
|
|
GL.Vertex(b0);
|
|
|
|
GL.Vertex(a1);
|
|
GL.Vertex(b0);
|
|
GL.Vertex(b1);
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>Draws triangular arrow.</summary>
|
|
/// <remarks>Must be called after <see cref="BeginTriangles"/> and before <see cref="EndTriangles"/>.</remarks>
|
|
public static void DrawArrowTriangleBatched(
|
|
Vector2 point,
|
|
Vector2 direction,
|
|
float width,
|
|
float length)
|
|
{
|
|
direction.Normalize();
|
|
|
|
var perpendicular = 0.5f * width * direction.GetPerpendicular();
|
|
|
|
// These commented out bits would use the point as the center of the triangle instead.
|
|
|
|
direction *= length;// * 0.5f;
|
|
|
|
var back = point - direction;
|
|
|
|
GL.Vertex(point);// + direction);
|
|
GL.Vertex(back + perpendicular);
|
|
GL.Vertex(back - perpendicular);
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>Returns a vector perpendicular to the given value with the same magnitude.</summary>
|
|
public static Vector2 GetPerpendicular(this Vector2 vector)
|
|
=> new(vector.y, -vector.x);
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>Draws a `sprite` in the given `area`.</summary>
|
|
public static void DrawSprite(Rect area, Sprite sprite)
|
|
{
|
|
var texture = sprite.texture;
|
|
var textureWidth = texture.width;
|
|
var textureHeight = texture.height;
|
|
var spriteRect = sprite.rect;
|
|
spriteRect.x /= textureWidth;
|
|
spriteRect.y /= textureHeight;
|
|
spriteRect.width /= textureWidth;
|
|
spriteRect.height /= textureHeight;
|
|
|
|
GUI.DrawTextureWithTexCoords(
|
|
area,
|
|
texture,
|
|
spriteRect);
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>Returns a colour with its hue based on the `hash`.</summary>
|
|
public static Color GetHashColor(int hash, float s = 1, float v = 1, float a = 1)
|
|
{
|
|
uint uHash = (uint)hash;
|
|
double dHash = (double)uHash / uint.MaxValue;
|
|
float h = (float)dHash;
|
|
var color = Color.HSVToRGB(h, s, v);
|
|
color.a = a;
|
|
return color;
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>Clears the <see cref="Selection.objects"/> then returns it to its current state.</summary>
|
|
/// <remarks>
|
|
/// This forces the <see cref="UnityEditorInternal.ReorderableList"/> drawer to adjust to height changes which
|
|
/// it unfortunately doesn't do on its own..
|
|
/// </remarks>
|
|
public static void ReSelectCurrentObjects()
|
|
{
|
|
var selection = Selection.objects;
|
|
Selection.objects = Array.Empty<Object>();
|
|
EditorApplication.delayCall += () =>
|
|
EditorApplication.delayCall += () =>
|
|
Selection.objects = selection;
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>Draws a button which toggles between play and pause icons.</summary>
|
|
public static bool DoPlayPauseToggle(
|
|
Rect area,
|
|
bool isPlaying,
|
|
GUIStyle style = null,
|
|
string tooltip = null)
|
|
{
|
|
var content = isPlaying
|
|
? AnimancerIcons.PauseIcon
|
|
: AnimancerIcons.PlayIcon;
|
|
|
|
var oldTooltip = content.tooltip;
|
|
content.tooltip = tooltip;
|
|
|
|
style ??= MiniButtonNoPadding;
|
|
|
|
if (GUI.Button(area, content, style))
|
|
isPlaying = !isPlaying;
|
|
|
|
content.tooltip = oldTooltip;
|
|
|
|
return isPlaying;
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
#endregion
|
|
/************************************************************************************************************************/
|
|
}
|
|
}
|
|
|
|
#endif
|
|
|