// MIT License - Copyright (c) 2025 wallstop
// Full license text: https://github.com/wallstop/unity-helpers/blob/main/LICENSE
namespace WallstopStudios.UnityHelpers.Editor.CustomDrawers
{
#if UNITY_EDITOR && ODIN_INSPECTOR
using System;
using Sirenix.OdinInspector.Editor;
using UnityEditor;
using UnityEngine;
using WallstopStudios.UnityHelpers.Core.Attributes;
using WallstopStudios.UnityHelpers.Core.Extension;
using WallstopStudios.UnityHelpers.Editor.CustomDrawers.Utils;
using WallstopStudios.UnityHelpers.Editor.Internal;
using Object = UnityEngine.Object;
///
/// Odin Inspector attribute drawer for .
/// Embeds the referenced object's inspector directly beneath the object field.
///
///
/// This drawer ensures WInLineEditor works correctly when Odin Inspector is installed
/// and classes derive from SerializedMonoBehaviour or SerializedScriptableObject,
/// where Unity's standard PropertyDrawer system is bypassed.
///
public sealed class WInLineEditorOdinDrawer : OdinAttributeDrawer
{
protected override void DrawPropertyLayout(GUIContent label)
{
WInLineEditorAttribute inlineAttribute = Attribute;
if (inlineAttribute == null)
{
CallNextDrawer(label);
return;
}
if (Property == null || Property.ValueEntry == null)
{
CallNextDrawer(label);
return;
}
Type valueType = Property.ValueEntry.TypeOfValue;
if (valueType == null || !typeof(Object).IsAssignableFrom(valueType))
{
CallNextDrawer(label);
return;
}
object weakValue = Property.ValueEntry.WeakSmartValue;
Object objectValue = weakValue as Object;
if (inlineAttribute.DrawObjectField)
{
CallNextDrawer(label);
}
else
{
if (label != null && label != GUIContent.none)
{
EditorGUILayout.LabelField(label);
}
}
if (objectValue == null)
{
return;
}
WInLineEditorMode resolvedMode = InLineEditorShared.ResolveMode(inlineAttribute);
string foldoutKey = BuildFoldoutKey();
bool isAlwaysExpanded = resolvedMode == WInLineEditorMode.AlwaysExpanded;
bool foldoutState = InLineEditorShared.GetFoldoutState(foldoutKey, resolvedMode);
bool showHeader =
InLineEditorShared.ShouldDrawStandaloneHeader(inlineAttribute)
&& (inlineAttribute.DrawHeader || !isAlwaysExpanded);
bool showBody = isAlwaysExpanded || foldoutState;
if (showHeader)
{
EditorGUILayout.Space(InLineEditorShared.Spacing);
foldoutState = DrawHeader(
objectValue,
label,
!isAlwaysExpanded,
foldoutState,
foldoutKey
);
}
if (showBody)
{
EditorGUILayout.Space(InLineEditorShared.Spacing);
DrawInlineInspector(objectValue, inlineAttribute);
}
if (inlineAttribute.DrawPreview && showBody)
{
EditorGUILayout.Space(InLineEditorShared.Spacing);
DrawPreview(objectValue, inlineAttribute.PreviewHeight);
}
}
private string BuildFoldoutKey()
{
InspectorProperty property = Property;
if (property == null)
{
return string.Empty;
}
object parent = property.Parent?.ValueEntry?.WeakSmartValue;
int parentId = 0;
if (parent is Object unityObject)
{
parentId = unityObject.GetInstanceID();
}
else if (parent != null)
{
parentId = parent.GetHashCode();
}
string path = property.Path;
return InLineEditorShared.BuildFoldoutKey(parentId, path);
}
private string BuildScrollKey()
{
return InLineEditorShared.BuildScrollKey(BuildFoldoutKey());
}
private static bool DrawHeader(
Object value,
GUIContent label,
bool showFoldoutToggle,
bool foldoutState,
string foldoutKey
)
{
Rect rect = EditorGUILayout.GetControlRect(false, InLineEditorShared.HeaderHeight);
float pingWidth = InLineEditorShared.GetPingButtonWidth();
bool showPingButton = InLineEditorShared.ShouldShowPingButton(value);
float headerSpacing = 0f;
float headerRightMargin = 0f;
if (showPingButton)
{
headerSpacing = InLineEditorShared.HeaderPingSpacing;
headerRightMargin = InLineEditorShared.PingButtonRightMargin;
bool hasSpace =
rect.width - pingWidth - headerSpacing - headerRightMargin
>= InLineEditorShared.MinimumFoldoutLabelWidth;
if (!hasSpace)
{
showPingButton = false;
headerSpacing = 0f;
headerRightMargin = 0f;
}
}
float labelWidth = showPingButton
? Mathf.Max(0f, rect.width - pingWidth - headerSpacing - headerRightMargin)
: rect.width;
Rect labelRect = new Rect(rect.x, rect.y, labelWidth, rect.height);
Rect pingRect = new Rect(
rect.x + labelWidth + (showPingButton ? headerSpacing : 0f),
rect.y,
pingWidth,
rect.height
);
GUIContent headerContent = InLineEditorShared.PrepareHeaderContent(value, label);
if (showFoldoutToggle)
{
bool newState = EditorGUI.Foldout(labelRect, foldoutState, headerContent, true);
if (newState != foldoutState)
{
foldoutState = newState;
InLineEditorShared.SetFoldoutState(foldoutKey, foldoutState);
}
}
else
{
EditorGUI.LabelField(labelRect, headerContent, EditorStyles.boldLabel);
}
if (showPingButton)
{
using (new EditorGUI.DisabledScope(value == null))
{
if (
GUI.Button(
pingRect,
InLineEditorShared.PingButtonContent,
EditorStyles.miniButton
)
)
{
EditorGUIUtility.PingObject(value);
}
}
}
return foldoutState;
}
private void DrawInlineInspector(Object value, WInLineEditorAttribute inlineAttribute)
{
Editor editor = InLineEditorShared.GetOrCreateEditor(value);
if (editor == null)
{
return;
}
float inspectorHeight = inlineAttribute.InspectorHeight;
string scrollKey = BuildScrollKey();
Vector2 scrollPosition = InLineEditorShared.GetScrollPosition(scrollKey);
bool enableScrolling = inlineAttribute.EnableScrolling;
bool scrollViewStarted = false;
bool verticalGroupStarted = false;
try
{
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
verticalGroupStarted = true;
if (enableScrolling)
{
scrollPosition = EditorGUILayout.BeginScrollView(
scrollPosition,
GUILayout.Height(inspectorHeight)
);
scrollViewStarted = true;
}
using (InlineInspectorContext.Enter())
{
editor.serializedObject.UpdateIfRequiredOrScript();
InLineEditorShared.DrawSerializedObject(editor.serializedObject);
}
if (scrollViewStarted)
{
EditorGUILayout.EndScrollView();
InLineEditorShared.SetScrollPosition(scrollKey, scrollPosition);
scrollViewStarted = false;
}
EditorGUILayout.EndVertical();
verticalGroupStarted = false;
}
catch (Exception e)
{
Debug.LogError($"Exception drawing inline inspector {e}.");
}
finally
{
if (scrollViewStarted)
{
EditorGUILayout.EndScrollView();
InLineEditorShared.SetScrollPosition(scrollKey, scrollPosition);
}
if (verticalGroupStarted)
{
EditorGUILayout.EndVertical();
}
}
}
private static void DrawPreview(Object value, float previewHeight)
{
if (value == null)
{
return;
}
Texture2D preview = AssetPreview.GetAssetPreview(value);
if (preview == null)
{
preview = AssetPreview.GetMiniThumbnail(value);
}
if (preview == null)
{
return;
}
Rect previewRect = EditorGUILayout.GetControlRect(false, previewHeight);
float aspectRatio = (float)preview.width / preview.height;
float previewWidth = Mathf.Min(previewRect.width, previewHeight * aspectRatio);
Rect centeredRect = new Rect(
previewRect.x + (previewRect.width - previewWidth) * 0.5f,
previewRect.y,
previewWidth,
previewHeight
);
GUI.DrawTexture(centeredRect, preview, ScaleMode.ScaleToFit);
}
///
/// Clears cached editors and state. Primarily for testing purposes.
///
internal static void ClearCachedStateForTesting()
{
InLineEditorShared.ClearCachedStateForTesting();
}
///
/// Test hook to set the foldout state for a given key.
///
internal static void SetFoldoutStateForTesting(string key, bool expanded)
{
InLineEditorShared.SetFoldoutStateForTesting(key, expanded);
}
///
/// Test hook to get the foldout state for a given key.
///
internal static bool GetFoldoutStateForTesting(string key)
{
return InLineEditorShared.GetFoldoutStateForTesting(key);
}
}
#endif
}