using System; using UnityEditor; using UnityEngine; namespace Unity.Mathematics.Editor { [CustomPropertyDrawer(typeof(bool2)), CustomPropertyDrawer(typeof(bool3)), CustomPropertyDrawer(typeof(bool4))] [CustomPropertyDrawer(typeof(double2)), CustomPropertyDrawer(typeof(double3)), CustomPropertyDrawer(typeof(double4))] [CustomPropertyDrawer(typeof(float2)), CustomPropertyDrawer(typeof(float3)), CustomPropertyDrawer(typeof(float4))] [CustomPropertyDrawer(typeof(int2)), CustomPropertyDrawer(typeof(int3)), CustomPropertyDrawer(typeof(int4))] [CustomPropertyDrawer(typeof(uint2)), CustomPropertyDrawer(typeof(uint3)), CustomPropertyDrawer(typeof(uint4))] [CustomPropertyDrawer(typeof(DoNotNormalizeAttribute))] class PrimitiveVectorDrawer : PropertyDrawer { private string _PropertyType; string GetPropertyType(SerializedProperty property) { if (_PropertyType == null) { _PropertyType = property.type; var isManagedRef = property.type.StartsWith("managedReference", StringComparison.Ordinal); if (isManagedRef) { var startIndex = "managedReference<".Length; var length = _PropertyType.Length - startIndex - 1; _PropertyType = _PropertyType.Substring("managedReference<".Length, length); } } return _PropertyType; } static class Content { public static readonly string doNotNormalizeCompatibility = L10n.Tr( $"{typeof(DoNotNormalizeAttribute).Name} only works with {typeof(quaternion)} and primitive vector types." ); public static readonly string doNotNormalizeTooltip = L10n.Tr("This value is not normalized, which may produce unexpected results."); public static readonly GUIContent[] labels2 = { new GUIContent("X"), new GUIContent("Y") }; public static readonly GUIContent[] labels3 = { new GUIContent("X"), new GUIContent("Y"), new GUIContent("Z") }; public static readonly GUIContent[] labels4 = { new GUIContent("X"), new GUIContent("Y"), new GUIContent("Z"), new GUIContent("W") }; } public override bool CanCacheInspectorGUI(SerializedProperty property) { return false; } public override float GetPropertyHeight(SerializedProperty property, GUIContent label) { var height = EditorGUIUtility.singleLineHeight; if (!EditorGUIUtility.wideMode) height += EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing; return height; } public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) { var subLabels = Content.labels4; var startIter = "x"; var propertyType = GetPropertyType(property); switch (propertyType[propertyType.Length - 1]) { case '2': subLabels = Content.labels2; break; case '3': subLabels = Content.labels3; break; case '4': subLabels = Content.labels4; break; default: { if (property.type == nameof(quaternion)) startIter = "value.x"; else if (attribute is DoNotNormalizeAttribute) { EditorGUI.HelpBox(EditorGUI.PrefixLabel(position, label), Content.doNotNormalizeCompatibility, MessageType.None); return; } break; } } if (attribute is DoNotNormalizeAttribute && string.IsNullOrEmpty(label.tooltip)) label.tooltip = Content.doNotNormalizeTooltip; label = EditorGUI.BeginProperty(position, label, property); var valuesIterator = property.FindPropertyRelative(startIter); MultiPropertyField(position, subLabels, valuesIterator, label); EditorGUI.EndProperty(); } void MultiPropertyField(Rect position, GUIContent[] subLabels, SerializedProperty valuesIterator, GUIContent label) { #if UNITY_2022_1_OR_NEWER EditorGUI.MultiPropertyField(position, subLabels, valuesIterator, label, EditorGUI.PropertyVisibility.All); #else EditorGUICopy.MultiPropertyField(position, subLabels, valuesIterator, label); #endif } } #if !UNITY_2022_1_OR_NEWER internal class EditorGUICopy { internal const float kSpacingSubLabel = 4; private const float kIndentPerLevel = 15; internal const float kPrefixPaddingRight = 2; internal static int indentLevel = 0; private static readonly int s_FoldoutHash = "Foldout".GetHashCode(); // internal static readonly SVC kVerticalSpacingMultiField = new SVC("--theme-multifield-vertical-spacing", 0.0f); // kVerticalSpacingMultiField should actually look like the above line ^^^ but we don't have access to SVC, // so instead we just set this value to what is observed in the debugger with the Unity dark theme. internal const float kVerticalSpacingMultiField = 2; internal enum PropertyVisibility { All, OnlyVisible } // This code is basically EditorGUI.MultiPropertyField(Rect, GUIContent[], SerializedProperty, GUIContent), // but with the property visibility assumed to be "All" instead of "OnlyVisible". We really want to have "All" // because it's possible for someone to hide something in the inspector with [HideInInspector] but then manually // draw it themselves later. In this case, if you called EditorGUI.MultiPropertyField() directly, you'd // end up with some fields that point to some unrelated visible property. public static void MultiPropertyField(Rect position, GUIContent[] subLabels, SerializedProperty valuesIterator, GUIContent label) { int id = GUIUtility.GetControlID(s_FoldoutHash, FocusType.Keyboard, position); position = MultiFieldPrefixLabel(position, id, label, subLabels.Length); position.height = EditorGUIUtility.singleLineHeight; MultiPropertyFieldInternal(position, subLabels, valuesIterator, PropertyVisibility.All); } internal static void BeginDisabled(bool disabled) { // Unused, but left here to minimize changes in EditorGUICopy.MultiPropertyFieldInternal(). } internal static void EndDisabled() { // Unused, but left here to minimize changes in EditorGUICopy.MultiPropertyFieldInternal(). } internal static float CalcPrefixLabelWidth(GUIContent label, GUIStyle style = null) { if (style == null) style = EditorStyles.label; return style.CalcSize(label).x; } internal static void MultiPropertyFieldInternal(Rect position, GUIContent[] subLabels, SerializedProperty valuesIterator, PropertyVisibility visibility, bool[] disabledMask = null, float prefixLabelWidth = -1) { int eCount = subLabels.Length; float w = (position.width - (eCount - 1) * kSpacingSubLabel) / eCount; Rect nr = new Rect(position) {width = w}; float t = EditorGUIUtility.labelWidth; int l = indentLevel; indentLevel = 0; for (int i = 0; i < subLabels.Length; i++) { EditorGUIUtility.labelWidth = prefixLabelWidth > 0 ? prefixLabelWidth : CalcPrefixLabelWidth(subLabels[i]); if (disabledMask != null) BeginDisabled(disabledMask[i]); EditorGUI.PropertyField(nr, valuesIterator, subLabels[i]); if (disabledMask != null) EndDisabled(); nr.x += w + kSpacingSubLabel; switch (visibility) { case PropertyVisibility.All: valuesIterator.Next(false); break; case PropertyVisibility.OnlyVisible: valuesIterator.NextVisible(false); break; } } EditorGUIUtility.labelWidth = t; indentLevel = l; } internal static bool LabelHasContent(GUIContent label) { if (label == null) { return true; } // @TODO: find out why checking for GUIContent.none doesn't work return label.text != string.Empty || label.image != null; } internal static float indent => indentLevel * kIndentPerLevel; internal static Rect MultiFieldPrefixLabel(Rect totalPosition, int id, GUIContent label, int columns) { if (!LabelHasContent(label)) { return EditorGUI.IndentedRect(totalPosition); } if (EditorGUIUtility.wideMode) { Rect labelPosition = new Rect(totalPosition.x + indent, totalPosition.y, EditorGUIUtility.labelWidth - indent, EditorGUIUtility.singleLineHeight); Rect fieldPosition = totalPosition; fieldPosition.xMin += EditorGUIUtility.labelWidth + kPrefixPaddingRight; // If there are 2 columns we use the same column widths as if there had been 3 columns // in order to make columns line up neatly. if (columns == 2) { float columnWidth = (fieldPosition.width - (3 - 1) * kSpacingSubLabel) / 3f; fieldPosition.xMax -= (columnWidth + kSpacingSubLabel); } EditorGUI.HandlePrefixLabel(totalPosition, labelPosition, label, id); return fieldPosition; } else { Rect labelPosition = new Rect(totalPosition.x + indent, totalPosition.y, totalPosition.width - indent, EditorGUIUtility.singleLineHeight); Rect fieldPosition = totalPosition; fieldPosition.xMin += indent + kIndentPerLevel; fieldPosition.yMin += EditorGUIUtility.singleLineHeight + kVerticalSpacingMultiField; EditorGUI.HandlePrefixLabel(totalPosition, labelPosition, label, id); return fieldPosition; } } } #endif }