2025-05-09 15:40:34 +08:00

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 &lt;= value &lt; 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