// MIT License - Copyright (c) 2025 wallstop
// Full license text: https://github.com/wallstop/unity-helpers/blob/main/LICENSE
namespace WallstopStudios.UnityHelpers.Editor.Utils
{
#if UNITY_EDITOR
using System;
using System.Collections.Generic;
using UnityEditor;
///
/// Provides allocation-optimized iteration over SerializedObject properties.
/// Uses GetIterator() to avoid repeated FindProperty() allocations.
///
internal static class SerializedPropertyIterator
{
private const string ScriptPropertyPath = "m_Script";
///
/// Iterates through all visible properties of a SerializedObject once,
/// invoking callbacks for each property based on whether it matches a set of paths to draw.
/// This avoids O(n) FindProperty calls by doing a single O(n) iteration.
///
/// The serialized object to iterate.
/// Set of property paths that should be drawn with the custom drawer.
/// Called when m_Script property is found (may be null if not found).
/// Called for each property whose path is in pathsToDraw.
/// Called for each property whose path is NOT in pathsToDraw.
internal static void IterateVisibleProperties(
SerializedObject serializedObject,
HashSet pathsToDraw,
Action scriptPropertyCallback,
Action matchedPropertyCallback,
Action unmatchedPropertyCallback
)
{
if (serializedObject == null)
{
return;
}
SerializedProperty iterator = serializedObject.GetIterator();
bool enterChildren = true;
while (iterator.NextVisible(enterChildren))
{
enterChildren = false;
string path = iterator.propertyPath;
if (string.Equals(path, ScriptPropertyPath, StringComparison.Ordinal))
{
scriptPropertyCallback?.Invoke(iterator);
continue;
}
if (pathsToDraw != null && pathsToDraw.Contains(path))
{
matchedPropertyCallback?.Invoke(iterator);
}
else
{
unmatchedPropertyCallback?.Invoke(iterator);
}
}
}
///
/// Iterates through all visible properties and collects copies into a pooled dictionary.
/// Use this when you need random access to properties by path.
/// Note: The returned properties are copies that remain valid after iteration.
///
/// The serialized object to iterate.
/// Dictionary to populate with property copies keyed by path.
/// If true, excludes the m_Script property.
internal static void BuildPropertyLookup(
SerializedObject serializedObject,
Dictionary propertyLookup,
bool excludeScript = true
)
{
if (serializedObject == null || propertyLookup == null)
{
return;
}
propertyLookup.Clear();
SerializedProperty iterator = serializedObject.GetIterator();
bool enterChildren = true;
while (iterator.NextVisible(enterChildren))
{
enterChildren = false;
string path = iterator.propertyPath;
if (
excludeScript
&& string.Equals(path, ScriptPropertyPath, StringComparison.Ordinal)
)
{
continue;
}
propertyLookup[path] = iterator.Copy();
}
}
///
/// Finds a specific property by iterating through visible properties.
/// More efficient than FindProperty when you only need one property and are already iterating.
///
/// The serialized object to search.
/// The path of the property to find.
/// A copy of the property if found, null otherwise.
internal static SerializedProperty FindPropertyByIteration(
SerializedObject serializedObject,
string propertyPath
)
{
if (serializedObject == null || string.IsNullOrEmpty(propertyPath))
{
return null;
}
SerializedProperty iterator = serializedObject.GetIterator();
bool enterChildren = true;
while (iterator.NextVisible(enterChildren))
{
enterChildren = false;
if (string.Equals(iterator.propertyPath, propertyPath, StringComparison.Ordinal))
{
return iterator.Copy();
}
}
return null;
}
///
/// Draws properties using a single iteration pass.
/// Groups are collected first, then drawn during iteration when their anchor property is encountered.
///
/// The serialized object.
/// Set of property paths that belong to groups (will be skipped in normal iteration).
/// Maps anchor property paths to their group definitions.
/// Callback to draw a non-grouped property.
/// Callback to draw groups anchored at a specific property.
/// Callback to draw the script property (or null to skip).
internal static void DrawPropertiesWithGroups(
SerializedObject serializedObject,
HashSet groupedPaths,
Dictionary> anchorToGroups,
Action drawProperty,
Action> drawGroups,
Action drawScriptProperty
)
{
if (serializedObject == null)
{
return;
}
SerializedProperty iterator = serializedObject.GetIterator();
bool enterChildren = true;
while (iterator.NextVisible(enterChildren))
{
enterChildren = false;
string path = iterator.propertyPath;
if (string.Equals(path, ScriptPropertyPath, StringComparison.Ordinal))
{
drawScriptProperty?.Invoke(iterator);
continue;
}
if (
anchorToGroups != null
&& anchorToGroups.TryGetValue(path, out List groups)
)
{
drawGroups?.Invoke(groups);
continue;
}
if (groupedPaths != null && groupedPaths.Contains(path))
{
continue;
}
drawProperty?.Invoke(iterator);
}
}
}
#endif
}