// MIT License - Copyright (c) 2025 wallstop // Full license text: https://github.com/wallstop/unity-helpers/blob/main/LICENSE namespace WallstopStudios.UnityHelpers.Editor.Utils.WGroup { #if UNITY_EDITOR using System; using System.Collections.Generic; using System.Reflection; using UnityEditor; using UnityEngine; using WallstopStudios.UnityHelpers.Core.Attributes; using WallstopStudios.UnityHelpers.Core.Extension; using WallstopStudios.UnityHelpers.Core.Helper; using WallstopStudios.UnityHelpers.Editor.Settings; using WallstopStudios.UnityHelpers.Utils; internal static class WGroupLayoutBuilder { private static readonly WGroupDrawOperation[] EmptyOperations = Array.Empty(); private static readonly WGroupDefinition[] EmptyDefinitions = Array.Empty(); private static readonly HashSet EmptyGroupedPaths = new(0); private static readonly HashSet EmptyHiddenPaths = new(0); private static readonly Dictionary< string, IReadOnlyList > EmptyAnchorToGroups = new(0); private static readonly Dictionary LayoutCache = new(); private static readonly Dictionary PropertyMetadataCache = new(); private static readonly List EmptyGroupAttributes = new(0); private static readonly List EmptyEndAttributes = new(0); private static readonly WGroupLayout EmptyLayout = new( EmptyOperations, EmptyDefinitions, new Dictionary(0), EmptyGroupedPaths, EmptyAnchorToGroups, EmptyHiddenPaths ); internal static void ClearCache() { LayoutCache.Clear(); PropertyMetadataCache.Clear(); } internal static WGroupLayout Build( SerializedObject serializedObject, string scriptPropertyPath ) { if (serializedObject == null) { throw new ArgumentNullException(nameof(serializedObject)); } UnityEngine.Object targetObject = serializedObject.targetObject; if (targetObject == null) { return EmptyLayout; } Type targetType = targetObject.GetType(); if (LayoutCache.TryGetValue(targetType, out WGroupLayout cachedLayout)) { return cachedLayout; } TypePropertyMetadata typeMetadata = GetOrCreatePropertyMetadata(targetType); if (typeMetadata.PropertyCount == 0) { LayoutCache[targetType] = EmptyLayout; return EmptyLayout; } WGroupLayout layout = BuildLayoutFromMetadata(typeMetadata, scriptPropertyPath); LayoutCache[targetType] = layout; return layout; } private static TypePropertyMetadata GetOrCreatePropertyMetadata(Type targetType) { if (PropertyMetadataCache.TryGetValue(targetType, out TypePropertyMetadata cached)) { return cached; } TypePropertyMetadata metadata = BuildTypePropertyMetadata(targetType); PropertyMetadataCache[targetType] = metadata; return metadata; } private static TypePropertyMetadata BuildTypePropertyMetadata(Type targetType) { List entries = new(); Type currentType = targetType; while (currentType != null && currentType != typeof(UnityEngine.Object)) { FieldInfo[] fields = currentType.GetFields( BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly ); foreach (FieldInfo field in fields) { if (!IsSerializableField(field)) { continue; } WGroupAttribute[] groupAttrs = field.GetAllAttributesSafe( inherit: true ); WGroupEndAttribute[] endAttrs = field.GetAllAttributesSafe( inherit: true ); List groupList = groupAttrs != null && groupAttrs.Length > 0 ? new List(groupAttrs) : EmptyGroupAttributes; List endList = endAttrs != null && endAttrs.Length > 0 ? new List(endAttrs) : EmptyEndAttributes; bool isHiddenInInspector = field.IsDefined(typeof(HideInInspector), false); entries.Add( new PropertyMetadataEntry( field.Name, groupList, endList, isHiddenInInspector ) ); } currentType = currentType.BaseType; } return new TypePropertyMetadata(entries); } private static bool IsSerializableField(FieldInfo field) { if (field.IsStatic || field.IsInitOnly) { return false; } if (field.IsPublic) { if (field.IsDefined(typeof(NonSerializedAttribute), false)) { return false; } return true; } if (field.IsDefined(typeof(SerializeField), false)) { return true; } if (field.IsDefined(typeof(SerializeReference), false)) { return true; } return false; } private static WGroupLayout BuildLayoutFromMetadata( TypePropertyMetadata typeMetadata, string scriptPropertyPath ) { IReadOnlyList entries = typeMetadata.Entries; if (entries.Count == 0) { return EmptyLayout; } UnityHelpersSettings.WGroupAutoIncludeConfiguration configuration = UnityHelpersSettings.GetWGroupAutoIncludeConfiguration(); AutoIncludeConfiguration globalConfiguration = ConvertConfiguration(configuration); WallstopGenericPool> contextsByNamePool = DictionaryBuffer.GetDictionaryPool( StringComparer.OrdinalIgnoreCase ); using PooledResource> contextsByNameLease = contextsByNamePool.Get(out Dictionary contextsByName); using PooledResource> contextsInDeclarationOrderLease = Buffers.GetList( entries.Count, out List contextsInDeclarationOrder ); using PooledResource> activeAutoContextsLease = Buffers.GetList(4, out List activeAutoContexts); List descriptors = new(entries.Count); for (int i = 0; i < entries.Count; i++) { PropertyMetadataEntry entry = entries[i]; if ( !string.IsNullOrEmpty(scriptPropertyPath) && string.Equals( entry.PropertyPath, scriptPropertyPath, StringComparison.Ordinal ) ) { continue; } descriptors.Add( new PropertyDescriptor( entry.PropertyPath, entry.GroupAttributes, entry.EndAttributes, entry.IsHiddenInInspector ) ); } if (descriptors.Count == 0) { return EmptyLayout; } for (int index = 0; index < descriptors.Count; index++) { PropertyDescriptor descriptor = descriptors[index]; HashSet explicitContexts = null; PooledResource> explicitContextsLease = default; if (descriptor.GroupAttributes.Count > 0) { explicitContextsLease = Buffers.HashSet.Get(out explicitContexts); foreach (WGroupAttribute attribute in descriptor.GroupAttributes) { string normalizedName = NormalizeGroupName(attribute.GroupName); if (!contextsByName.TryGetValue(normalizedName, out GroupContext context)) { context = new GroupContext( normalizedName, contextsInDeclarationOrder.Count ); contextsByName.Add(normalizedName, context); contextsInDeclarationOrder.Add(context); } context.ApplyAttribute(attribute, descriptor.PropertyPath, index); AutoIncludeConfiguration localConfiguration = ResolveAutoInclude( attribute.AutoIncludeCount, globalConfiguration ); context.SetAutoInclude(localConfiguration); UpdateActiveContextList(activeAutoContexts, context); explicitContexts.Add(context); } } if (explicitContexts == null || explicitContexts.Count == 0) { // Skip auto-include for HideInInspector fields - they should not be // automatically added to groups, only explicitly included via [WGroup] if (!descriptor.IsHiddenInInspector) { GroupContext autoContext = SelectAutoIncludeTarget( activeAutoContexts, explicitContexts ); if (autoContext != null) { bool added = autoContext.AddProperty(descriptor.PropertyPath, index); if (added) { autoContext.ConsumeAutoInclude(); UpdateActiveContextList(activeAutoContexts, autoContext); } } } } else { // Add to explicit contexts foreach (GroupContext context in explicitContexts) { context.AddProperty(descriptor.PropertyPath, index); } // Add anchor property to the direct parent group (if any) // This ensures nested group anchors are included in their parent groups foreach (WGroupAttribute attribute in descriptor.GroupAttributes) { if (string.IsNullOrEmpty(attribute.ParentGroup)) { continue; } string normalizedParentName = NormalizeGroupName(attribute.ParentGroup); if ( !contextsByName.TryGetValue( normalizedParentName, out GroupContext parentContext ) ) { continue; } // Don't add if already in explicit contexts if (explicitContexts.Contains(parentContext)) { continue; } bool added = parentContext.AddProperty(descriptor.PropertyPath, index); if (added && parentContext.HasAutoIncludeBudget) { parentContext.ConsumeAutoInclude(); } } } if (descriptor.EndAttributes.Count > 0) { ApplyGroupEnds(descriptor.EndAttributes, activeAutoContexts, contextsByName); } explicitContextsLease.Dispose(); } List definitions = new(contextsInDeclarationOrder.Count); WallstopGenericPool>> groupsByAnchorPool = DictionaryBuffer>.GetDictionaryPool( StringComparer.Ordinal ); using PooledResource>> groupsByAnchorLease = groupsByAnchorPool.Get( out Dictionary> groupsByAnchor ); Dictionary groupsByName = new( StringComparer.OrdinalIgnoreCase ); HashSet groupedPaths = new(StringComparer.Ordinal); Dictionary> anchorToGroupsTemp = new( StringComparer.Ordinal ); foreach (GroupContext context in contextsInDeclarationOrder) { if (context.PropertyCount == 0) { continue; } WGroupDefinition definition = context.ToDefinition(); definitions.Add(definition); groupsByName[definition.Name] = definition; List anchored = groupsByAnchor.GetOrAdd( definition.AnchorPropertyPath ); anchored.Add(definition); foreach (string path in definition.PropertyPaths) { groupedPaths.Add(path); } if ( !anchorToGroupsTemp.TryGetValue( definition.AnchorPropertyPath, out List anchorList ) ) { anchorList = new List(); anchorToGroupsTemp[definition.AnchorPropertyPath] = anchorList; } anchorList.Add(definition); } // Build parent-child relationships for nested groups HashSet circularRefs = null; foreach (WGroupDefinition definition in definitions) { if (string.IsNullOrEmpty(definition.ParentGroupName)) { continue; } string normalizedParentName = NormalizeGroupName(definition.ParentGroupName); if ( !groupsByName.TryGetValue( normalizedParentName, out WGroupDefinition parentDefinition ) ) { // Parent not found - treat as top-level (already handled by null ParentGroupName check) continue; } // Check for circular reference if (HasCircularReference(definition, groupsByName)) { circularRefs ??= new HashSet(StringComparer.OrdinalIgnoreCase); if (circularRefs.Add(definition.Name)) { Debug.LogWarning( $"[WGroup] Circular reference detected for group '{definition.Name}' with parent '{definition.ParentGroupName}'. Treating as top-level group." ); } continue; } parentDefinition.AddChildGroup(definition); } // Sort child groups by declaration order and calculate direct property paths foreach (WGroupDefinition definition in definitions) { definition.SortChildGroups(); // Calculate direct property paths (excluding child group anchor paths) if (definition.ChildGroups.Count > 0) { HashSet childAnchorPaths = new(StringComparer.Ordinal); foreach (WGroupDefinition child in definition.ChildGroups) { childAnchorPaths.Add(child.AnchorPropertyPath); } List directPaths = new(definition.PropertyPaths.Count); foreach (string path in definition.PropertyPaths) { if (!childAnchorPaths.Contains(path)) { directPaths.Add(path); } } definition.SetDirectPropertyPaths(directPaths); } else { // No child groups, all paths are direct definition.SetDirectPropertyPaths(definition.PropertyPaths); } } Dictionary> anchorToGroups = new( anchorToGroupsTemp.Count, StringComparer.Ordinal ); foreach (KeyValuePair> kvp in anchorToGroupsTemp) { kvp.Value.Sort( (left, right) => left.DeclarationOrder.CompareTo(right.DeclarationOrder) ); anchorToGroups[kvp.Key] = kvp.Value; } List operations = BuildDrawOperations( descriptors, groupsByAnchor, groupsByName, out HashSet hiddenPropertyPaths ); return new WGroupLayout( operations, definitions, groupsByName, groupedPaths, anchorToGroups, hiddenPropertyPaths ); } private static AutoIncludeConfiguration ConvertConfiguration( UnityHelpersSettings.WGroupAutoIncludeConfiguration configuration ) { switch (configuration.Mode) { case UnityHelpersSettings.WGroupAutoIncludeMode.Infinite: return new AutoIncludeConfiguration(true, 0); case UnityHelpersSettings.WGroupAutoIncludeMode.Finite: return new AutoIncludeConfiguration(false, configuration.RowCount); default: return new AutoIncludeConfiguration(false, 0); } } private static AutoIncludeConfiguration ResolveAutoInclude( int requestedValue, AutoIncludeConfiguration globalConfiguration ) { if (requestedValue == WGroupAttribute.UseGlobalAutoInclude) { return globalConfiguration; } if (requestedValue == WGroupAttribute.InfiniteAutoInclude) { return new AutoIncludeConfiguration(true, 0); } if (requestedValue <= 0) { return new AutoIncludeConfiguration(false, 0); } return new AutoIncludeConfiguration(false, requestedValue); } private static void UpdateActiveContextList( List activeAutoContexts, GroupContext context ) { for (int index = 0; index < activeAutoContexts.Count; index++) { if (ReferenceEquals(activeAutoContexts[index], context)) { activeAutoContexts.RemoveAt(index); break; } } if (!context.HasAutoIncludeBudget) { return; } InsertActiveContext(activeAutoContexts, context); } private static void InsertActiveContext( List activeAutoContexts, GroupContext context ) { for (int index = 0; index < activeAutoContexts.Count; index++) { GroupContext existing = activeAutoContexts[index]; if (existing.DeclarationOrder > context.DeclarationOrder) { activeAutoContexts.Insert(index, context); return; } } activeAutoContexts.Add(context); } private static GroupContext SelectAutoIncludeTarget( List activeAutoContexts, HashSet explicitContexts ) { for (int index = activeAutoContexts.Count - 1; index >= 0; index--) { GroupContext candidate = activeAutoContexts[index]; if (!candidate.HasAutoIncludeBudget) { activeAutoContexts.RemoveAt(index); continue; } if (explicitContexts != null && explicitContexts.Contains(candidate)) { continue; } return candidate; } return null; } private static void ApplyGroupEnds( List endAttributes, List activeAutoContexts, Dictionary contextsByName ) { for (int index = 0; index < endAttributes.Count; index++) { WGroupEndAttribute attribute = endAttributes[index]; IReadOnlyList groupNames = attribute.GroupNames; if (groupNames.Count == 0) { if (activeAutoContexts.Count > 0) { GroupContext last = activeAutoContexts[^1]; last.SetAutoInclude(new AutoIncludeConfiguration(false, 0)); UpdateActiveContextList(activeAutoContexts, last); } continue; } for (int nameIndex = 0; nameIndex < groupNames.Count; nameIndex++) { string groupName = groupNames[nameIndex]; if (string.IsNullOrEmpty(groupName)) { continue; } string normalizedName = NormalizeGroupName(groupName); if (!contextsByName.TryGetValue(normalizedName, out GroupContext context)) { continue; } context.SetAutoInclude(new AutoIncludeConfiguration(false, 0)); UpdateActiveContextList(activeAutoContexts, context); } } } private static string NormalizeGroupName(string groupName) { return string.IsNullOrWhiteSpace(groupName) ? string.Empty : groupName.Trim(); } private static List BuildDrawOperations( List descriptors, Dictionary> groupsByAnchor, Dictionary groupsByName, out HashSet hiddenPropertyPaths ) { List operations = new(descriptors.Count); hiddenPropertyPaths = new HashSet(StringComparer.Ordinal); WallstopGenericPool> consumedPool = SetBuffers.GetHashSetPool( StringComparer.Ordinal ); using PooledResource> consumedLease = consumedPool.Get( out HashSet consumed ); { for (int index = 0; index < descriptors.Count; index++) { PropertyDescriptor descriptor = descriptors[index]; string propertyPath = descriptor.PropertyPath; bool anchoredHandled = false; if ( groupsByAnchor.TryGetValue( propertyPath, out List anchoredGroups ) ) { anchoredHandled = true; anchoredGroups.Sort( (left, right) => left.DeclarationOrder.CompareTo(right.DeclarationOrder) ); foreach (WGroupDefinition definition in anchoredGroups) { // Skip child groups - they will be rendered by their parent in WGroupGUI if (definition.HasParent) { // Verify parent exists and this is actually a child string normalizedParentName = NormalizeGroupName( definition.ParentGroupName ); if ( groupsByName.TryGetValue( normalizedParentName, out WGroupDefinition parentDef ) && parentDef.ChildGroups.Contains(definition) ) { // Mark all properties as consumed but don't add operation for ( int memberIndex = 0; memberIndex < definition.PropertyPaths.Count; memberIndex++ ) { consumed.Add(definition.PropertyPaths[memberIndex]); } continue; } } operations.Add(new WGroupDrawOperation(definition)); for ( int memberIndex = 0; memberIndex < definition.PropertyPaths.Count; memberIndex++ ) { consumed.Add(definition.PropertyPaths[memberIndex]); } } } if (anchoredHandled) { continue; } if (!consumed.Add(propertyPath)) { continue; } bool isHidden = descriptor.IsHiddenInInspector; if (isHidden) { hiddenPropertyPaths.Add(propertyPath); } operations.Add(new WGroupDrawOperation(propertyPath, isHidden)); } } return operations; } /// /// Checks if a group definition has a circular reference in its parent chain. /// private static bool HasCircularReference( WGroupDefinition definition, Dictionary groupsByName ) { HashSet visited = new(StringComparer.OrdinalIgnoreCase) { definition.Name }; string currentParentName = definition.ParentGroupName; while (!string.IsNullOrEmpty(currentParentName)) { string normalizedName = NormalizeGroupName(currentParentName); if (!visited.Add(normalizedName)) { // Already visited this group - circular reference detected return true; } if (!groupsByName.TryGetValue(normalizedName, out WGroupDefinition parentDef)) { // Parent not found, no circular reference possible break; } currentParentName = parentDef.ParentGroupName; } return false; } private sealed class GroupContext { private readonly List _entries = new(); private readonly HashSet _lookup = new(StringComparer.Ordinal); private bool _hasExplicitStartCollapsed; private WGroupAttribute.WGroupCollapseBehavior _collapseBehavior = WGroupAttribute .WGroupCollapseBehavior .UseProjectSetting; internal GroupContext(string name, int declarationOrder) { Name = name; DeclarationOrder = declarationOrder; AnchorIndex = int.MaxValue; } internal string Name { get; } internal int DeclarationOrder { get; } internal string DisplayName { get; private set; } internal bool Collapsible { get; private set; } internal bool StartCollapsed { get; private set; } internal bool HideHeader { get; private set; } internal int AnchorIndex { get; private set; } internal string AnchorPropertyPath { get; private set; } internal bool AutoIncludeInfinite { get; private set; } internal int RemainingAutoInclude { get; private set; } internal string ParentGroupName { get; private set; } internal int PropertyCount { get { return _entries.Count; } } internal void ApplyAttribute( WGroupAttribute attribute, string propertyPath, int propertyIndex ) { // Only update DisplayName if the attribute has an explicitly set display name // (not just the fallback to GroupName) if ( !string.IsNullOrWhiteSpace(attribute.DisplayName) && !string.Equals( attribute.DisplayName, attribute.GroupName, StringComparison.Ordinal ) ) { DisplayName = attribute.DisplayName; } // Capture parent group name if specified if (!string.IsNullOrWhiteSpace(attribute.ParentGroup)) { ParentGroupName = attribute.ParentGroup.Trim(); } if (Collapsible != attribute.Collapsible) { Collapsible = attribute.Collapsible; if (!Collapsible) { StartCollapsed = false; _hasExplicitStartCollapsed = false; _collapseBehavior = WGroupAttribute .WGroupCollapseBehavior .UseProjectSetting; } } if (attribute.Collapsible) { _collapseBehavior = attribute.CollapseBehavior; switch (_collapseBehavior) { case WGroupAttribute.WGroupCollapseBehavior.ForceCollapsed: StartCollapsed = true; _hasExplicitStartCollapsed = true; break; case WGroupAttribute.WGroupCollapseBehavior.ForceExpanded: StartCollapsed = false; _hasExplicitStartCollapsed = true; break; default: if (!_hasExplicitStartCollapsed) { StartCollapsed = UnityHelpersSettings.ShouldStartWGroupCollapsed(); } break; } } HideHeader = attribute.HideHeader; AddProperty(propertyPath, propertyIndex); } internal void SetAutoInclude(AutoIncludeConfiguration configuration) { AutoIncludeInfinite = configuration.IsInfinite; RemainingAutoInclude = configuration.IsInfinite ? 0 : configuration.Count; } internal bool AddProperty(string propertyPath, int propertyIndex) { if (!_lookup.Add(propertyPath)) { return false; } PropertyEntry entry = new(propertyPath, propertyIndex); _entries.Add(entry); if (propertyIndex < AnchorIndex) { AnchorIndex = propertyIndex; AnchorPropertyPath = propertyPath; } return true; } internal void ConsumeAutoInclude() { if (AutoIncludeInfinite) { return; } if (RemainingAutoInclude <= 0) { return; } RemainingAutoInclude--; } internal bool HasAutoIncludeBudget { get { if (AutoIncludeInfinite) { return true; } return RemainingAutoInclude > 0; } } internal WGroupDefinition ToDefinition() { _entries.Sort((left, right) => left.PropertyIndex.CompareTo(right.PropertyIndex)); List orderedPaths = new(_entries.Count); for (int index = 0; index < _entries.Count; index++) { orderedPaths.Add(_entries[index].PropertyPath); } string displayName = DisplayName ?? Name; string anchorPath = AnchorPropertyPath ?? orderedPaths[0]; int anchorIndex = AnchorIndex == int.MaxValue ? 0 : AnchorIndex; return new WGroupDefinition( Name, displayName, Collapsible, StartCollapsed, HideHeader, orderedPaths, anchorPath, anchorIndex, DeclarationOrder, ParentGroupName ); } private readonly struct PropertyEntry { internal PropertyEntry(string propertyPath, int propertyIndex) { PropertyPath = propertyPath; PropertyIndex = propertyIndex; } internal string PropertyPath { get; } internal int PropertyIndex { get; } } } private sealed class PropertyDescriptor { internal PropertyDescriptor( string propertyPath, List groupAttributes, List endAttributes, bool isHiddenInInspector = false ) { PropertyPath = propertyPath; GroupAttributes = groupAttributes ?? new List(); EndAttributes = endAttributes ?? new List(); IsHiddenInInspector = isHiddenInInspector; } internal string PropertyPath { get; } internal List GroupAttributes { get; } internal List EndAttributes { get; } internal bool IsHiddenInInspector { get; } } private readonly struct AutoIncludeConfiguration { internal AutoIncludeConfiguration(bool isInfinite, int count) { IsInfinite = isInfinite; Count = count < 0 ? 0 : count; } internal bool IsInfinite { get; } internal int Count { get; } } } internal enum WGroupDrawOperationType { Property = 0, Group = 1, } internal readonly struct WGroupDrawOperation { internal WGroupDrawOperation(string propertyPath, bool isHiddenInInspector = false) { Type = WGroupDrawOperationType.Property; PropertyPath = propertyPath; Group = null; IsHiddenInInspector = isHiddenInInspector; } internal WGroupDrawOperation(WGroupDefinition group) { Type = WGroupDrawOperationType.Group; PropertyPath = null; Group = group; IsHiddenInInspector = false; } internal WGroupDrawOperationType Type { get; } internal string PropertyPath { get; } internal WGroupDefinition Group { get; } /// /// Whether this property has [HideInInspector] attribute. /// Only relevant for Property operations. /// internal bool IsHiddenInInspector { get; } } internal sealed class WGroupDefinition { private static readonly List EmptyChildGroups = new(0); private static readonly IReadOnlyList EmptyDirectPropertyPaths = Array.Empty(); private List _childGroups; private IReadOnlyList _directPropertyPaths; internal WGroupDefinition( string name, string displayName, bool collapsible, bool startCollapsed, bool hideHeader, IReadOnlyList propertyPaths, string anchorPropertyPath, int anchorIndex, int declarationOrder, string parentGroupName = null ) { Name = name; DisplayName = displayName; Collapsible = collapsible; StartCollapsed = startCollapsed; HideHeader = hideHeader; PropertyPaths = propertyPaths; AnchorPropertyPath = anchorPropertyPath; AnchorIndex = anchorIndex; DeclarationOrder = declarationOrder; ParentGroupName = parentGroupName; _childGroups = null; _directPropertyPaths = null; } internal string Name { get; } internal string DisplayName { get; } internal bool Collapsible { get; } internal bool StartCollapsed { get; } internal bool HideHeader { get; } internal IReadOnlyList PropertyPaths { get; } internal string AnchorPropertyPath { get; } internal int AnchorIndex { get; } internal int DeclarationOrder { get; } /// /// The name of the parent group, or null for top-level groups. /// internal string ParentGroupName { get; } /// /// Child groups to be rendered inside this group. Populated after construction. /// internal List ChildGroups { get => _childGroups ?? EmptyChildGroups; } /// /// Property paths excluding child group anchor paths. /// internal IReadOnlyList DirectPropertyPaths { get => _directPropertyPaths ?? EmptyDirectPropertyPaths; } /// /// Returns true if this group has a parent group. /// internal bool HasParent => !string.IsNullOrEmpty(ParentGroupName); /// /// Adds a child group to this group's ChildGroups list. /// internal void AddChildGroup(WGroupDefinition child) { _childGroups ??= new List(); _childGroups.Add(child); } /// /// Sorts child groups by their declaration order. /// internal void SortChildGroups() { _childGroups?.Sort( (left, right) => left.DeclarationOrder.CompareTo(right.DeclarationOrder) ); } /// /// Sets the direct property paths (excluding child anchor paths). /// internal void SetDirectPropertyPaths(IReadOnlyList paths) { _directPropertyPaths = paths ?? EmptyDirectPropertyPaths; } } internal sealed class WGroupLayout { internal WGroupLayout( IReadOnlyList operations, IReadOnlyList groups, IReadOnlyDictionary groupsByName, IReadOnlyCollection groupedPaths, IReadOnlyDictionary> anchorToGroups, IReadOnlyCollection hiddenPropertyPaths ) { Operations = operations; Groups = groups; GroupsByName = groupsByName; GroupedPaths = groupedPaths; AnchorToGroups = anchorToGroups; HiddenPropertyPaths = hiddenPropertyPaths; } internal IReadOnlyList Operations { get; } internal IReadOnlyList Groups { get; } internal IReadOnlyDictionary GroupsByName { get; } /// /// All property paths that belong to any group. Used for quick lookup during iteration. /// internal IReadOnlyCollection GroupedPaths { get; } /// /// Maps anchor property paths to the groups that should be drawn at that anchor. /// internal IReadOnlyDictionary< string, IReadOnlyList > AnchorToGroups { get; } /// /// All property paths that have [HideInInspector] attribute. /// Used for quick lookup during drawing to skip hidden properties. /// internal IReadOnlyCollection HiddenPropertyPaths { get; } internal bool TryGetGroup(string groupName, out WGroupDefinition definition) { if (string.IsNullOrWhiteSpace(groupName)) { definition = null; return false; } return GroupsByName.TryGetValue(groupName.Trim(), out definition); } } internal sealed class TypePropertyMetadata { internal TypePropertyMetadata(List entries) { Entries = entries; } internal IReadOnlyList Entries { get; } internal int PropertyCount => Entries.Count; } internal readonly struct PropertyMetadataEntry { internal PropertyMetadataEntry( string propertyPath, List groupAttributes, List endAttributes, bool isHiddenInInspector = false ) { PropertyPath = propertyPath; GroupAttributes = groupAttributes; EndAttributes = endAttributes; IsHiddenInInspector = isHiddenInInspector; } internal string PropertyPath { get; } internal List GroupAttributes { get; } internal List EndAttributes { get; } internal bool IsHiddenInInspector { get; } } #endif }