using System.Collections.Generic;
/// Fallback inspector for all MonoBehaviours, but only in the UnchartedLimbo namespace.
/// Groups fields under each [Header("…")] into a collapsible foldout, exits on [Space][Space],
/// and otherwise behaves exactly like Unity’s default inspector.
[CustomEditor(typeof(MonoBehaviour), true, true)] // the third ‘true’ makes it a fallback
public class HeaderGroupEditor : Editor
//––– Persistent, shared state so foldouts “remember” per-object, even when you re-select –––
// Keyed by $"{instanceID}_{headerString}"
private static Dictionary<string,bool> s_FoldoutStates = new Dictionary<string,bool>();
// Bold‐style foldout (arrow + bold text)
private GUIStyle _boldFoldoutStyle;
// Clone Unity’s default foldout style and make it bold
_boldFoldoutStyle = new GUIStyle(EditorStyles.foldout)
fontStyle = FontStyle.Bold
public override void OnInspectorGUI()
// 1) Namespace guard: only UnchartedLimbo.*
var t = target.GetType();
if (t.Namespace == null || !t.Namespace.StartsWith("UnchartedLimbo"))
serializedObject.Update();
// 2) Draw the “Script” field first (always present on MonoBehaviour)
var prop = serializedObject.GetIterator();
bool enterChildren = true;
if (prop.NextVisible(enterChildren))
EditorGUILayout.PropertyField(prop, true);
// State for the current group
string currentHeader = null;
// 3) Walk through every other visible property in declaration order
while (prop.NextVisible(false))
// Use reflection to grab any Header or Space attributes on this field
FieldInfo fi = GetFieldInfo(prop);
var headerAttr = fi?.GetCustomAttribute<HeaderAttribute>(true);
var spaceAttrs = fi?.GetCustomAttributes(typeof(SpaceAttribute), true);
bool isHeader = headerAttr != null;
bool isExitMarker = (spaceAttrs != null && spaceAttrs.Length >= 2);
// --- Start a new foldout on [Header] ---
// If we were already in a group, close its indent
currentHeader = headerAttr.header;
// Build a per-object key so each instance “remembers” its state
string key = $"{target.GetInstanceID()}_{currentHeader}";
if (!s_FoldoutStates.TryGetValue(key, out currentOpen))
s_FoldoutStates[key] = currentOpen;
// Draw the foldout header (arrow + bold label)
currentOpen = EditorGUILayout.Foldout(
true, // toggle when clicking text
_boldFoldoutStyle // our bold style
// Save back the new open/closed state
s_FoldoutStates[key] = currentOpen;
// Indent everything inside this group
// ** Draw the very first field (the one that had [Header]) **
// using its **normal** label (prop.displayName) so it isn’t repeated
EditorGUILayout.PropertyField(prop, true);
// --- Exit the group if we see [Space][Space] on a single field ---
// Always draw this field normally
EditorGUILayout.PropertyField(prop, true);
// --- Otherwise: draw inside the group only if it’s open; or outside if no group ---
EditorGUILayout.PropertyField(prop, true);
EditorGUILayout.PropertyField(prop, true);
// Unindent if we ended inside a group
serializedObject.ApplyModifiedProperties();
/// Helper: given a SerializedProperty, finds the corresponding FieldInfo (handles arrays/lists/nesting).
private FieldInfo GetFieldInfo(SerializedProperty prop)
// Turn “myList.Array.data[0].someField” → “myList[0].someField”
string path = prop.propertyPath.Replace(".Array.data[", "[");
var elements = path.Split('.');
System.Type type = target.GetType();
foreach (var element in elements)
int bracket = element.IndexOf('[');
name = element.Substring(0, bracket);
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic
// Drill down into the element type if it’s an array or List<T>
type = type.GetElementType();
else if (type.IsGenericType
&& type.GetGenericTypeDefinition() == typeof(List<>))
type = type.GetGenericArguments()[0];