// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2023 Kybernetik // #if UNITY_EDITOR using System; using UnityEditor; using UnityEngine; using UnityEngine.Playables; using Object = UnityEngine.Object; using static Animancer.Editor.AnimancerGUI; namespace Animancer.Editor { /// [Editor-Only] Draws the Inspector GUI for an . /// https://kybernetik.com.au/animancer/api/Animancer.Editor/IAnimancerNodeDrawer /// public interface IAnimancerNodeDrawer { /// Draws the details and controls for the target node in the Inspector. void DoGUI(); } /************************************************************************************************************************/ /// [Editor-Only] Draws the Inspector GUI for an . /// https://kybernetik.com.au/animancer/api/Animancer.Editor/AnimancerNodeDrawer_1 /// public abstract class AnimancerNodeDrawer : IAnimancerNodeDrawer where T : AnimancerNode { /************************************************************************************************************************/ /// The node being managed. public T Target { get; protected set; } /// If true, the details of the will be expanded in the Inspector. public ref bool IsExpanded => ref Target._IsInspectorExpanded; /************************************************************************************************************************/ /// The used for the area encompassing this drawer. protected abstract GUIStyle RegionStyle { get; } /************************************************************************************************************************/ /// Draws the details and controls for the target in the Inspector. public virtual void DoGUI() { if (!Target.IsValid) return; BeginVerticalBox(RegionStyle); { DoHeaderGUI(); DoDetailsGUI(); } EndVerticalBox(RegionStyle); CheckContextMenu(GUILayoutUtility.GetLastRect()); } /************************************************************************************************************************/ /// /// Draws the name and other details of the in the GUI. /// protected virtual void DoHeaderGUI() { var area = LayoutSingleLineRect(SpacingMode.Before); DoLabelGUI(area); DoFoldoutGUI(area); } /// /// Draws a field for the if it has one, otherwise just a simple text /// label. /// protected abstract void DoLabelGUI(Rect area); /// Draws a foldout arrow to expand/collapse the node details. protected abstract void DoFoldoutGUI(Rect area); /// Draws the details of the in the GUI. protected abstract void DoDetailsGUI(); /************************************************************************************************************************/ /// /// Draws controls for , , and /// . /// protected void DoNodeDetailsGUI() { var area = LayoutSingleLineRect(SpacingMode.Before); area.xMin += EditorGUI.indentLevel * IndentSize; var xMin = area.xMin; var labelWidth = EditorGUIUtility.labelWidth; var indentLevel = EditorGUI.indentLevel; EditorGUI.indentLevel = 0; // Is Playing. var state = Target as AnimancerState; if (state != null) { var buttonArea = StealFromLeft(ref area, PlayButtonWidth, StandardSpacing); var content = state.IsPlaying ? PauseButtonContent : PlayButtonContent; if (CompactMiniButton(buttonArea, content)) state.IsPlaying = !state.IsPlaying; } SplitHorizontally(area, "Speed", "Weight", out var speedWidth, out var weightWidth, out var speedRect, out var weightRect); // Speed. EditorGUIUtility.labelWidth = speedWidth; EditorGUI.BeginChangeCheck(); var speed = EditorGUI.FloatField(speedRect, "Speed", Target.Speed); if (EditorGUI.EndChangeCheck()) Target.Speed = speed; if (TryUseClickEvent(speedRect, 2)) Target.Speed = Target.Speed != 1 ? 1 : 0; // Weight. EditorGUIUtility.labelWidth = weightWidth; EditorGUI.BeginChangeCheck(); var weight = EditorGUI.FloatField(weightRect, "Weight", Target.Weight); if (EditorGUI.EndChangeCheck()) SetWeight(Mathf.Max(weight, 0)); if (TryUseClickEvent(weightRect, 2)) SetWeight(Target.Weight != 1 ? 1 : 0); // Not really sure why this is necessary. // It allows the dummy ID added when the Real Speed is hidden to work properly. GUIUtility.GetControlID(FocusType.Passive); // Real Speed (Mixer Synchronization changes the internal Playable Speed without setting the State Speed). speed = (float)Target._Playable.GetSpeed(); if (Target.Speed != speed) { using (new EditorGUI.DisabledScope(true)) { area = LayoutSingleLineRect(SpacingMode.Before); area.xMin = xMin; var label = BeginTightLabel("Real Speed"); EditorGUIUtility.labelWidth = CalculateLabelWidth(label); EditorGUI.FloatField(area, label, speed); EndTightLabel(); } } else// Add a dummy ID so that subsequent IDs don't change when the Real Speed appears or disappears. { GUIUtility.GetControlID(FocusType.Passive); } EditorGUI.indentLevel = indentLevel; EditorGUIUtility.labelWidth = labelWidth; DoFadeDetailsGUI(); } /************************************************************************************************************************/ /// Indicates whether changing the should normalize its siblings. protected virtual bool AutoNormalizeSiblingWeights => false; private void SetWeight(float weight) { if (weight < 0 || weight > 1 || Mathf.Approximately(Target.Weight, 1) || !AutoNormalizeSiblingWeights) goto JustSetWeight; var parent = Target.Parent; if (parent == null) goto JustSetWeight; var totalWeight = 0f; var siblingCount = parent.ChildCount; for (int i = 0; i < siblingCount; i++) { var sibling = parent.GetChild(i); if (sibling.IsValid()) totalWeight += sibling.Weight; } // If the weights weren't previously normalized, don't normalize them now. if (!Mathf.Approximately(totalWeight, 1)) goto JustSetWeight; var siblingWeightMultiplier = (totalWeight - weight) / (totalWeight - Target.Weight); for (int i = 0; i < siblingCount; i++) { var sibling = parent.GetChild(i); if (sibling != Target && sibling.IsValid()) sibling.Weight *= siblingWeightMultiplier; } JustSetWeight: Target.Weight = weight; } /************************************************************************************************************************/ /// Draws the and . private void DoFadeDetailsGUI() { var area = LayoutSingleLineRect(SpacingMode.Before); area = EditorGUI.IndentedRect(area); var speedLabel = GetNarrowText("Fade Speed"); var targetLabel = GetNarrowText("Target Weight"); SplitHorizontally(area, speedLabel, targetLabel, out var speedWidth, out var weightWidth, out var speedRect, out var weightRect); var labelWidth = EditorGUIUtility.labelWidth; var indentLevel = EditorGUI.indentLevel; EditorGUI.indentLevel = 0; EditorGUI.BeginChangeCheck(); // Fade Speed. EditorGUIUtility.labelWidth = speedWidth; Target.FadeSpeed = EditorGUI.DelayedFloatField(speedRect, speedLabel, Target.FadeSpeed); if (TryUseClickEvent(speedRect, 2)) { Target.FadeSpeed = Target.FadeSpeed != 0 || AnimancerPlayable.DefaultFadeDuration == 0 ? 0 : Math.Abs(Target.Weight - Target.TargetWeight) / AnimancerPlayable.DefaultFadeDuration; } // Target Weight. EditorGUIUtility.labelWidth = weightWidth; Target.TargetWeight = Mathf.Max(0, EditorGUI.FloatField(weightRect, targetLabel, Target.TargetWeight)); if (TryUseClickEvent(weightRect, 2)) { if (Target.TargetWeight != Target.Weight) Target.TargetWeight = Target.Weight; else if (Target.TargetWeight != 1) Target.TargetWeight = 1; else Target.TargetWeight = 0; } if (EditorGUI.EndChangeCheck() && Target.FadeSpeed != 0) Target.StartFade(Target.TargetWeight, 1 / Target.FadeSpeed); EditorGUI.indentLevel = indentLevel; EditorGUIUtility.labelWidth = labelWidth; } /************************************************************************************************************************/ #region Context Menu /************************************************************************************************************************/ /// /// The menu label prefix used for details about the . /// protected const string DetailsPrefix = "Details/"; /// /// Checks if the current event is a context menu click within the `clickArea` and opens a context menu with various /// functions for the . /// protected void CheckContextMenu(Rect clickArea) { if (!TryUseClickEvent(clickArea, 1)) return; var menu = new GenericMenu(); menu.AddDisabledItem(new GUIContent(Target.ToString())); PopulateContextMenu(menu); menu.AddItem(new GUIContent(DetailsPrefix + "Log Details"), false, () => Debug.Log(Target.GetDescription(), Target.Root?.Component as Object)); menu.AddItem(new GUIContent(DetailsPrefix + "Log Details Of Everything"), false, () => Debug.Log(Target.Root.GetDescription(), Target.Root?.Component as Object)); AnimancerPlayableDrawer.AddPlayableGraphVisualizerFunction(menu, DetailsPrefix, Target.Root._Graph); menu.ShowAsContext(); } /// Adds functions relevant to the . protected abstract void PopulateContextMenu(GenericMenu menu); /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ } } #endif