// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2023 Kybernetik // #if UNITY_EDITOR using System; using System.Collections; using System.Runtime.CompilerServices; using UnityEditor; using UnityEngine; using Object = UnityEngine.Object; namespace Animancer.Editor { /// [Editor-Only] Various GUI utilities used throughout Animancer. /// https://kybernetik.com.au/animancer/api/Animancer.Editor/AnimancerGUI /// 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) }; /************************************************************************************************************************/ /// Returns . public static float LineHeight => EditorGUIUtility.singleLineHeight; /************************************************************************************************************************/ /// Returns . public static float StandardSpacing => 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 => GUI.skin.label.normal.textColor; /************************************************************************************************************************/ private static GUIStyle _MiniButton; /// A more compact with a fixed size as a tiny box. public static GUIStyle MiniButton { get { if (_MiniButton == null) { _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 }; } return _MiniButton; } } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region Layout /************************************************************************************************************************/ /// Calls . public static void RepaintEverything() => UnityEditorInternal.InternalEditorUtility.RepaintAllViews(); /************************************************************************************************************************/ /// Indicates where should add the . public enum SpacingMode { /// No extra space. None, /// Add extra space before the new area. Before, /// Add extra space after the new area. After, /// Add extra space before and after the new area. BeforeAndAfter } /// /// 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.height -= StandardSpacing; return rect; case SpacingMode.BeforeAndAfter: rect = GUILayoutUtility.GetRect(0, LineHeight + StandardSpacing * 2); rect.yMin += StandardSpacing; rect.height -= StandardSpacing; return rect; default: throw new ArgumentException($"Unsupported {nameof(StandardSpacing)}: " + spacing, nameof(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; } /************************************************************************************************************************/ /// [Animancer Extension] Calls and returns the max width. public static float CalculateWidth(this GUIStyle style, GUIContent content) { style.CalcMinMaxWidth(content, out _, out var width); return Mathf.Ceil(width); } /// [Animancer Extension] Calls and returns the max width. public static float CalculateWidth(this GUIStyle style, string text) { using (ObjectPool.Disposable.AcquireContent(out var content, text, null, false)) return style.CalculateWidth(content); } /************************************************************************************************************************/ /// /// Creates a for calculating the GUI width occupied by text using the /// specified `style`. /// public static ConversionCache CreateWidthCache(GUIStyle style) => new ConversionCache((text) => style.CalculateWidth(text)); /************************************************************************************************************************/ private static ConversionCache _LabelWidthCache; /// /// Calls using and returns the max /// width. The result is cached for efficient reuse. /// 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(); } /************************************************************************************************************************/ /// Clears the then returns it to its current state. /// /// This forces the drawer to adjust to height changes which /// it unfortunately doesn't do on its own.. /// public static void ReSelectCurrentObjects() { var selection = Selection.objects; Selection.objects = Array.Empty(); EditorApplication.delayCall += () => EditorApplication.delayCall += () => Selection.objects = selection; } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region Labels /************************************************************************************************************************/ private static GUIStyle _WeightLabelStyle; private static float _WeightLabelWidth = -1; /// /// 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) { var label = WeightToShortString(weight, out var isExact); if (_WeightLabelStyle == null) _WeightLabelStyle = new GUIStyle(GUI.skin.label); if (_WeightLabelWidth < 0) { _WeightLabelStyle.fontStyle = FontStyle.Italic; _WeightLabelWidth = _WeightLabelStyle.CalculateWidth("0.0"); } _WeightLabelStyle.normal.textColor = Color.Lerp(Color.grey, TextColor, weight); _WeightLabelStyle.fontStyle = isExact ? FontStyle.Normal : FontStyle.Italic; var weightArea = StealFromRight(ref area, _WeightLabelWidth); GUI.Label(weightArea, label, _WeightLabelStyle); } /************************************************************************************************************************/ private static ConversionCache _ShortWeightCache; /// Returns a string which approximates the `weight` into no more than 3 digits. 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 "???"; if (_ShortWeightCache == null) _ShortWeightCache = new ConversionCache((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); } /************************************************************************************************************************/ /// 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); } /************************************************************************************************************************/ /// Loads an icon texture and sets it to use . public static Texture LoadIcon(string name) { var icon = (Texture)EditorGUIUtility.Load(name); if (icon != null) icon.filterMode = FilterMode.Bilinear; return icon; } /************************************************************************************************************************/ /// Calls if the `content` was null. public static GUIContent IconContent(ref GUIContent content, string name) { if (content == null) content = EditorGUIUtility.IconContent(name); return content; } /************************************************************************************************************************/ /// Draws a button using and . public static bool CompactMiniButton(GUIContent content) => GUILayout.Button(content, EditorStyles.miniButton, DontExpandWidth); /// Draws a button using . public static bool CompactMiniButton(Rect area, GUIContent content) => GUI.Button(area, content, EditorStyles.miniButton); /************************************************************************************************************************/ private static GUIContent _PlayButtonContent, _PauseButtonContent, _StepBackwardButtonContent, _StepForwardButtonContent; /// for a play button. public static GUIContent PlayButtonContent => IconContent(ref _PlayButtonContent, "PlayButton"); /// for a pause button. public static GUIContent PauseButtonContent => IconContent(ref _PauseButtonContent, "PauseButton"); /// for a step backward button. public static GUIContent StepBackwardButtonContent => IconContent(ref _StepBackwardButtonContent, "Animation.PrevKey"); /// for a step forward button. public static GUIContent StepForwardButtonContent => IconContent(ref _StepForwardButtonContent, "Animation.NextKey"); /************************************************************************************************************************/ private static float _PlayButtonWidth; /// The default width of using . public static float PlayButtonWidth { get { if (_PlayButtonWidth <= 0) EditorStyles.miniButton.CalcMinMaxWidth(PlayButtonContent, out _PlayButtonWidth, out _); return _PlayButtonWidth; } } /************************************************************************************************************************/ #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)) return false; GUI.changed = true; currentEvent.Use(); if (currentEvent.button == 2) Deselect(); return true; } /// /// 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) => 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, DragAndDropVisualMode mode = DragAndDropVisualMode.Link) 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; } TryDrop(DragAndDrop.objectReferences, validate, onDrop, isDrop, mode); } /************************************************************************************************************************/ /// /// Updates the or calls `onDrop` for each of the `objects`. /// private static void TryDrop(IEnumerable objects, Func validate, Action onDrop, bool isDrop, DragAndDropVisualMode mode) 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))) { Deselect(); if (!isDrop) { DragAndDrop.visualMode = mode; 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, DragAndDropVisualMode mode = DragAndDropVisualMode.Link) { HandleDragAndDrop(dropArea, (clip) => !clip.legacy, onDrop, mode); HandleDragAndDrop(dropArea, null, (source) => { using (ObjectPool.Disposable.AcquireList(out var clips)) { source.GetAnimationClips(clips); TryDrop(clips, (clip) => !clip.legacy, onDrop, true, mode); } }, mode); HandleDragAndDrop(dropArea, null, (collection) => { using (ObjectPool.Disposable.AcquireSet(out var clips)) { collection.GatherAnimationClips(clips); TryDrop(clips, (clip) => !clip.legacy, onDrop, true, mode); } }, mode); } /************************************************************************************************************************/ /// Deselects any selected IMGUI control. public static void Deselect() => GUIUtility.keyboardControl = 0; /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ } } #endif