// MIT License - Copyright (c) 2025 wallstop // Full license text: https://github.com/wallstop/unity-helpers/blob/main/LICENSE namespace WallstopStudios.UnityHelpers.Core.Attributes { using System; /// /// Declares a boxed inspector grouping (similar to Odin's BoxGroup) that can automatically sweep subsequent members into the same visual section. /// /// /// /// Use when you want to present related fields together without manually repeating the attribute on each member. /// The parameter determines how many following serialized members are automatically captured. /// Combine it with to stop inclusion early or skip particular fields. /// /// /// Groups can be toggled collapsible, assigned theme colors via palette keys, and rendered without headers for lightweight inline layouts. /// /// /// Groups can be nested inside other groups by specifying a parentGroup parameter. The parent group must be declared /// before or on the same property as the child group. Nested groups are rendered indented within their parent's visual container. /// /// /// /// Collapsible box group with auto inclusion: /// /// public sealed class WeaponStats : MonoBehaviour /// { /// [WGroup("Damage", displayName: "Damage Settings", autoIncludeCount: 2, collapsible: true)] /// public int lightAttackDamage; /// /// public int heavyAttackDamage; /// public float critMultiplier; /// /// [WGroup("Damage"), WGroupEnd] /// public AnimationCurve falloff; /// } /// /// Nested groups example: /// /// public sealed class CharacterData : MonoBehaviour /// { /// [WGroup("outer", "Character")] /// public string characterName; /// /// [WGroup("inner", "Stats", parentGroup: "outer")] // Nested inside "outer" /// public int level; /// public int experience; /// } /// /// [AttributeUsage( AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = true, Inherited = true )] public sealed class WGroupAttribute : Attribute { /// /// Represents how collapsible groups determine their default foldout state. /// public enum WGroupCollapseBehavior { /// /// Uses the Unity Helpers project setting to decide whether the header starts collapsed. /// UseProjectSetting = 0, /// /// Forces the header to start expanded regardless of project defaults. /// ForceExpanded = 1, /// /// Forces the header to start collapsed regardless of project defaults. /// ForceCollapsed = 2, } /// /// Sentinel value instructing the drawer to keep auto including members until a matching is reached. /// public const int InfiniteAutoInclude = -1; /// /// Sentinel value that tells the drawer to fall back to the global auto include count defined in the Unity Helpers settings asset. /// public const int UseGlobalAutoInclude = -2; /// /// Creates a new grouped inspector section. /// /// Unique key that ties and entries together. /// Optional heading shown in the inspector. Defaults to . /// /// Number of serialized members after the annotated field that should automatically join the group. /// Use to keep including until a is hit. /// /// Set to to draw the box with a foldout. /// When collapsible, controls whether the group starts closed. /// Set to to draw the group body without the title bar. /// /// Optional name of another group that this group should be nested inside. /// The parent group must be declared before or on the same property as this group. /// /// Thrown when is null or whitespace. public WGroupAttribute( string groupName, string displayName = null, int autoIncludeCount = UseGlobalAutoInclude, bool collapsible = false, bool startCollapsed = false, bool hideHeader = false, string parentGroup = null ) { if (string.IsNullOrWhiteSpace(groupName)) { throw new ArgumentException( "Group name cannot be null or whitespace.", nameof(groupName) ); } GroupName = groupName.Trim(); DisplayName = string.IsNullOrWhiteSpace(displayName) ? GroupName : displayName.Trim(); AutoIncludeCount = NormalizeAutoIncludeCount(autoIncludeCount); Collapsible = collapsible; if (startCollapsed) { CollapseBehavior = WGroupCollapseBehavior.ForceCollapsed; } HideHeader = hideHeader; ParentGroup = string.IsNullOrWhiteSpace(parentGroup) ? null : parentGroup.Trim(); } /// /// Identifier shared between the start and end attributes. /// public string GroupName { get; } /// /// Human-readable title drawn in the inspector (defaults to ). /// public string DisplayName { get; } /// /// Number of trailing serialized members automatically swept into the group. /// public int AutoIncludeCount { get; } /// /// Gets a value indicating whether the group can be collapsed with a foldout toggle. /// public bool Collapsible { get; } /// /// Gets or sets how the attribute resolves its initial collapse state. /// public WGroupCollapseBehavior CollapseBehavior { get; set; } = WGroupCollapseBehavior.UseProjectSetting; /// /// Gets a value indicating whether a collapsible group should start closed. /// public bool StartCollapsed { get { return CollapseBehavior == WGroupCollapseBehavior.ForceCollapsed; } } /// /// Set to to hide the header while still wrapping the grouped fields inside the styled container. /// public bool HideHeader { get; } /// /// Gets the name of the parent group that this group should be nested inside, or if this is a top-level group. /// public string ParentGroup { get; } private static int NormalizeAutoIncludeCount(int autoIncludeCount) { if (autoIncludeCount < InfiniteAutoInclude) { return UseGlobalAutoInclude; } if (autoIncludeCount == InfiniteAutoInclude) { return InfiniteAutoInclude; } return autoIncludeCount < 0 ? UseGlobalAutoInclude : autoIncludeCount; } } }