// 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;
}
}
}