// MIT License - Copyright (c) 2025 wallstop // Full license text: https://github.com/wallstop/unity-helpers/blob/main/LICENSE namespace WallstopStudios.UnityHelpers.Editor.CustomDrawers.Utils { #if UNITY_EDITOR using System; using System.Collections.Generic; using System.Globalization; using System.Runtime.CompilerServices; using UnityEditor; using UnityEngine; using WallstopStudios.UnityHelpers.Core.Helper; using WallstopStudios.UnityHelpers.Editor.Core.Helper; using WallstopStudios.UnityHelpers.Editor.Settings; using WallstopStudios.UnityHelpers.Editor.Utils.WButton; /// /// Provides shared constants, types, and helper methods for enum toggle button drawers. /// /// /// This utility class consolidates common code used by both the standard PropertyDrawer /// and the Odin Inspector drawer implementations of WEnumToggleButtons. By centralizing /// these elements, we ensure consistent behavior and eliminate code duplication. /// public static class EnumToggleButtonsShared { /// /// Default padding around buttons. /// public const float ButtonPadding = 6f; /// /// Padding around selected items. /// public const float SelectionPadding = 8f; /// /// Default toolbar height. /// public const float ToolbarHeight = 68f; /// /// Height of selected item display. /// public const float SelectedItemHeight = 60f; /// /// Height of labels. /// public const float LabelHeight = 22f; /// /// Minimum width for buttons. /// public const float MinButtonWidth = 80f; /// /// Vertical spacing between button rows. /// public const float ButtonVerticalSpacing = 2f; /// /// Horizontal spacing between buttons. /// public const float ButtonHorizontalSpacing = 5f; /// /// Spacing between toolbar elements. /// public const float ToolbarSpacing = 6f; /// /// Gap between toolbar buttons. /// public const float ToolbarButtonGap = 8f; /// /// Minimum width for toolbar buttons. /// public const float ToolbarButtonMinWidth = 60f; /// /// Width of pagination navigation buttons. /// public const float PaginationButtonWidth = 22f; /// /// Minimum width for pagination label. /// public const float PaginationLabelMinWidth = 80f; /// /// Spacing above and below summary text. /// public const float SummarySpacing = 2f; /// /// Ratio used to split available width evenly between two aligned buttons (e.g., Select All / None). /// public const float EqualSplitRatio = 0.5f; /// /// Maximum ratio of the pagination area width allocated to each navigation button. /// Ensures buttons don't become excessively wide in large layouts. /// public const float MaxPaginationButtonWidthRatio = 0.2f; /// /// Ratio used to center elements when distributing overflow correction. /// Applies half the overflow adjustment to shift elements toward center. /// public const float OverflowCenteringRatio = 0.5f; /// /// Content for navigating to previous page. /// public static readonly GUIContent PrevPageContent = new("◀", "Previous Page"); /// /// Content for navigating to next page. /// public static readonly GUIContent NextPageContent = new("▶", "Next Page"); /// /// Content for the "None" selection button. /// public static readonly GUIContent NoneContent = new("None"); /// /// Content for the "All" selection button. /// public static readonly GUIContent AllContent = new("All"); /// /// Content for expand indicator. /// public static readonly GUIContent ExpandContent = new("▼"); /// /// Content for collapse indicator. /// public static readonly GUIContent CollapseContent = new("▲"); /// /// Content for search indicator. /// public static readonly GUIContent SearchContent = new("🔍", "Search"); // Lazy initialization to avoid calling EditorGUIUtility during static class loading, // which can hang Unity during "Open Project: Open Scene" if the class is accessed // before EditorGUIUtility is fully initialized. private static GUIContent _firstPageContent; private static GUIContent _lastPageContent; /// /// Content for navigating to first page. /// public static GUIContent FirstPageContent => _firstPageContent ??= EditorGUIUtility.TrTextContent("<<", "First Page"); /// /// Content for navigating to last page. /// public static GUIContent LastPageContent => _lastPageContent ??= EditorGUIUtility.TrTextContent(">>", "Last Page"); private static readonly Dictionary ButtonStyleCache = new( new ButtonStyleCacheKeyComparer() ); private static GUIStyle _summaryStyle; private const string SummaryStyleKey = "EnumToggleButtons/SummaryStyle"; /// /// Gets the style used for displaying selection summary text. /// public static GUIStyle SummaryStyle { get { if (_summaryStyle != null) { return _summaryStyle; } _summaryStyle = EditorCacheHelper.GetOrCreateStyle( SummaryStyleKey, CreateSummaryStyle ); return _summaryStyle; } } private static GUIStyle CreateSummaryStyle() { return new GUIStyle(EditorStyles.wordWrappedMiniLabel) { fontStyle = FontStyle.Italic }; } /// /// Determines which segment style a button should use based on its position. /// /// The zero-based index of the button in the visible set. /// The total number of visible buttons. /// The number of columns in the layout. /// The appropriate for the button position. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ButtonSegment ResolveButtonSegment(int index, int total, int columns) { if (columns <= 1) { return ButtonSegment.Single; } int columnIndex = index % columns; bool isFirst = columnIndex == 0; bool isLast = columnIndex == columns - 1 || index == total - 1; if (isFirst && isLast) { return ButtonSegment.Single; } if (isFirst) { return ButtonSegment.Left; } if (isLast) { return ButtonSegment.Right; } return ButtonSegment.Middle; } /// /// Calculates the layout metrics for displaying toggle buttons. /// /// The requested number of buttons per row, or 0 for auto. /// The total number of options to display. /// The available width for the button layout. /// The height of a single button row. /// The spacing between buttons. /// The minimum width for a single button. /// A describing the calculated layout. public static LayoutMetrics CalculateLayout( int requestedPerRow, int optionCount, float availableWidth, float lineHeight, float spacing, float minWidth ) { if (optionCount <= 0) { return new LayoutMetrics(1, 0, minWidth, lineHeight, spacing, 0f); } if (optionCount == 1) { float singleWidth = Mathf.Max(minWidth, availableWidth); return new LayoutMetrics(1, 1, singleWidth, lineHeight, spacing, lineHeight); } int columns = requestedPerRow > 0 ? requestedPerRow : DetermineAutoColumns(availableWidth, spacing, minWidth); columns = Mathf.Clamp(columns, 1, optionCount); int rows = Mathf.CeilToInt(optionCount / (float)columns); float workingWidth = availableWidth; if (workingWidth <= 0f) { workingWidth = columns * minWidth + (columns - 1) * spacing; } float buttonWidth = (workingWidth - (columns - 1) * spacing) / columns; if (buttonWidth < minWidth) { buttonWidth = minWidth; } float totalHeight = rows * lineHeight + Mathf.Max(0, rows - 1) * spacing; return new LayoutMetrics(columns, rows, buttonWidth, lineHeight, spacing, totalHeight); } /// /// Determines the optimal number of columns based on available width. /// /// The available width for the layout. /// The spacing between buttons. /// The minimum width for each button. /// The optimal number of columns, at least 1. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int DetermineAutoColumns(float availableWidth, float spacing, float minWidth) { if (availableWidth <= 0f) { return 1; } float effectiveWidth = minWidth + spacing; int columns = Mathf.FloorToInt((availableWidth + spacing) / effectiveWidth); if (columns < 1) { columns = 1; } return columns; } /// /// Checks if a value is a power of two. /// /// The value to check. /// True if the value is a power of two and not zero; otherwise, false. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsPowerOfTwo(ulong value) { return value != 0UL && (value & (value - 1UL)) == 0UL; } /// /// Converts an object value to a UInt64 representation. /// /// The value to convert (typically an enum value). /// The UInt64 representation, or 0 if conversion fails. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ulong ConvertToUInt64(object value) { if (value == null) { return 0UL; } try { return Convert.ToUInt64(value, CultureInfo.InvariantCulture); } catch { return 0UL; } } /// /// Gets a cached button style for the specified segment and state. /// /// The button segment (Single, Left, Middle, Right). /// Whether the button is in an active (selected) state. /// The background color when selected. /// The text color when selected. /// The background color when not selected. /// The text color when not selected. /// A cached configured for the button. public static GUIStyle GetButtonStyle( ButtonSegment segment, bool isActive, Color selectedBg, Color selectedText, Color inactiveBg, Color inactiveText ) { ButtonStyleCacheKey key = new( segment, isActive, selectedBg, selectedText, inactiveBg, inactiveText ); if (ButtonStyleCache.TryGetValue(key, out GUIStyle cached)) { return cached; } GUIStyle basis = segment switch { ButtonSegment.Left => EditorStyles.miniButtonLeft, ButtonSegment.Middle => EditorStyles.miniButtonMid, ButtonSegment.Right => EditorStyles.miniButtonRight, _ => EditorStyles.miniButton, }; GUIStyle style = new(basis) { name = "EnumToggleButtons/" + segment.ToString() + "/" + (isActive ? "Active" : "Inactive"), }; Color baseBackground = isActive ? selectedBg : inactiveBg; Color hoverBackground = WButtonColorUtility.GetHoverColor(baseBackground); Color activeBackground = WButtonColorUtility.GetActiveColor(baseBackground); Color textColor = isActive ? selectedText : inactiveText; ConfigureButtonStyle( style, baseBackground, hoverBackground, activeBackground, textColor ); ButtonStyleCache[key] = style; return style; } /// /// Gets a cached button style using a palette entry. /// /// The button segment (Single, Left, Middle, Right). /// Whether the button is in an active (selected) state. /// The color palette entry to use. /// A cached configured for the button. public static GUIStyle GetButtonStyle( ButtonSegment segment, bool isActive, UnityHelpersSettings.WEnumToggleButtonsPaletteEntry palette ) { return GetButtonStyle( segment, isActive, palette.SelectedBackgroundColor, palette.SelectedTextColor, palette.InactiveBackgroundColor, palette.InactiveTextColor ); } /// /// Configures a GUIStyle with the specified colors for all states. /// /// The style to configure. /// The background color for normal state. /// The background color for hover state. /// The background color for active/pressed state. /// The text color for all states. public static void ConfigureButtonStyle( GUIStyle style, Color normalBg, Color hoverBg, Color activeBg, Color textColor ) { if (style == null) { return; } Texture2D normalTexture = EditorCacheHelper.GetOrCreateTexture(normalBg); Texture2D hoverTexture = EditorCacheHelper.GetOrCreateTexture(hoverBg); Texture2D activeTexture = EditorCacheHelper.GetOrCreateTexture(activeBg); style.normal.background = normalTexture; style.normal.textColor = textColor; style.focused.background = normalTexture; style.focused.textColor = textColor; style.onNormal.background = normalTexture; style.onNormal.textColor = textColor; style.onFocused.background = normalTexture; style.onFocused.textColor = textColor; style.hover.background = hoverTexture; style.hover.textColor = textColor; style.onHover.background = hoverTexture; style.onHover.textColor = textColor; style.active.background = activeTexture; style.active.textColor = textColor; style.onActive.background = activeTexture; style.onActive.textColor = textColor; } /// /// Clears all cached button styles. Call when theme or colors change. /// public static void ClearStyleCache() { ButtonStyleCache.Clear(); _summaryStyle = null; } /// /// Defines the visual segment of a button in a horizontal toolbar layout. /// public enum ButtonSegment { /// A standalone button with no adjacent buttons. Single = 0, /// The leftmost button in a group. Left = 1, /// A middle button in a group. Middle = 2, /// The rightmost button in a group. Right = 3, } /// /// Cache key for button styles, incorporating all visual parameters. /// public readonly struct ButtonStyleCacheKey : IEquatable { /// /// Gets the button segment type. /// public ButtonSegment Segment { get; } /// /// Gets whether the button is in an active state. /// public bool IsActive { get; } /// /// Gets the selected background color. /// public Color SelectedBackground { get; } /// /// Gets the selected text color. /// public Color SelectedText { get; } /// /// Gets the inactive background color. /// public Color InactiveBackground { get; } /// /// Gets the inactive text color. /// public Color InactiveText { get; } /// /// Creates a new button style cache key. /// /// The button segment type. /// Whether the button is active. /// The selected background color. /// The selected text color. /// The inactive background color. /// The inactive text color. public ButtonStyleCacheKey( ButtonSegment segment, bool isActive, Color selectedBackground, Color selectedText, Color inactiveBackground, Color inactiveText ) { Segment = segment; IsActive = isActive; SelectedBackground = selectedBackground; SelectedText = selectedText; InactiveBackground = inactiveBackground; InactiveText = inactiveText; } /// /// Creates a new button style cache key from a palette entry. /// /// The button segment type. /// Whether the button is active. /// The color palette entry. public ButtonStyleCacheKey( ButtonSegment segment, bool isActive, UnityHelpersSettings.WEnumToggleButtonsPaletteEntry palette ) { Segment = segment; IsActive = isActive; SelectedBackground = palette.SelectedBackgroundColor; SelectedText = palette.SelectedTextColor; InactiveBackground = palette.InactiveBackgroundColor; InactiveText = palette.InactiveTextColor; } /// public bool Equals(ButtonStyleCacheKey other) { return Segment == other.Segment && IsActive == other.IsActive && EditorCacheHelper.AreColorsEqual( SelectedBackground, other.SelectedBackground ) && EditorCacheHelper.AreColorsEqual(SelectedText, other.SelectedText) && EditorCacheHelper.AreColorsEqual( InactiveBackground, other.InactiveBackground ) && EditorCacheHelper.AreColorsEqual(InactiveText, other.InactiveText); } /// public override bool Equals(object obj) { return obj is ButtonStyleCacheKey other && Equals(other); } /// public override int GetHashCode() { return Objects.HashCode( Segment, IsActive, SelectedBackground.r, SelectedBackground.g, SelectedBackground.b, SelectedBackground.a, SelectedText.r, SelectedText.g, SelectedText.b, SelectedText.a, InactiveBackground.r, InactiveBackground.g, InactiveBackground.b, InactiveBackground.a, InactiveText.r, InactiveText.g, InactiveText.b, InactiveText.a ); } } /// /// Comparer for used in dictionary lookups. /// public sealed class ButtonStyleCacheKeyComparer : IEqualityComparer { /// public bool Equals(ButtonStyleCacheKey x, ButtonStyleCacheKey y) { return x.Equals(y); } /// public int GetHashCode(ButtonStyleCacheKey obj) { return obj.GetHashCode(); } } /// /// Represents a single toggle option in an enum toggle button drawer. /// public readonly struct ToggleOption { /// /// Gets the display label for the option. /// public string Label { get; } /// /// Gets the original enum value. /// public object Value { get; } /// /// Gets the numeric flag value as UInt64. /// public ulong FlagValue { get; } /// /// Gets whether this represents a zero-valued flag (typically "None"). /// public bool IsZeroFlag { get; } /// /// Creates a new toggle option. /// /// The display label. /// The enum value. /// The numeric flag value. /// Whether this is a zero flag. public ToggleOption(string label, object value, ulong flagValue, bool isZeroFlag) { Label = string.IsNullOrEmpty(label) ? "(Unnamed)" : label; Value = value; FlagValue = flagValue; IsZeroFlag = isZeroFlag; } } /// /// Represents a summary of selected items not visible on the current page. /// public readonly struct SelectionSummary { /// /// Gets a summary indicating no out-of-view selections. /// public static SelectionSummary None { get; } = new(false, GUIContent.none); /// /// Gets whether there is summary content to display. /// public bool HasSummary { get; } /// /// Gets the GUI content to display. /// public GUIContent Content { get; } /// /// Creates a new selection summary. /// /// Whether there is summary content. /// The content to display. public SelectionSummary(bool hasSummary, GUIContent content) { HasSummary = hasSummary; Content = content ?? GUIContent.none; } } /// /// Describes the calculated layout metrics for displaying toggle buttons. /// public readonly struct LayoutMetrics { /// /// Gets the number of columns in the layout. /// public int Columns { get; } /// /// Gets the number of rows in the layout. /// public int Rows { get; } /// /// Gets the width of each button. /// public float ButtonWidth { get; } /// /// Gets the height of each button. /// public float ButtonHeight { get; } /// /// Gets the spacing between buttons. /// public float Spacing { get; } /// /// Gets the total height of the button layout. /// public float TotalHeight { get; } /// /// Creates new layout metrics. /// /// The number of columns. /// The number of rows. /// The button width. /// The button height. /// The spacing between buttons. /// The total layout height. public LayoutMetrics( int columns, int rows, float buttonWidth, float buttonHeight, float spacing, float totalHeight ) { Columns = columns; Rows = rows; ButtonWidth = buttonWidth; ButtonHeight = buttonHeight; Spacing = spacing; TotalHeight = totalHeight; } /// /// Gets the rectangle for a button at the specified index. /// /// The bounding rectangle for the entire button area. /// The zero-based index of the button. /// The rectangle where the button should be drawn. [MethodImpl(MethodImplOptions.AggressiveInlining)] public Rect GetItemRect(Rect bounds, int index) { int row = index / Columns; int column = index % Columns; float x = bounds.x + column * (ButtonWidth + Spacing); float y = bounds.y + row * (ButtonHeight + Spacing); return new Rect(x, y, ButtonWidth, ButtonHeight); } } /// /// Tracks pagination state for a property. /// public sealed class PaginationState { private int _pageIndex; /// /// Gets or sets the number of items per page. /// public int PageSize { get; set; } /// /// Gets or sets the total number of items. /// public int TotalItems { get; set; } /// /// Gets or sets the current page index (0-based). /// public int PageIndex { get => _pageIndex; set => _pageIndex = value; } /// /// Gets the total number of pages. /// public int TotalPages { get { if (PageSize <= 0) { return 1; } return Mathf.Max(1, Mathf.CeilToInt(TotalItems / (float)PageSize)); } } /// /// Gets the starting index of items on the current page. /// public int StartIndex { get { if (TotalItems <= 0 || PageSize <= 0) { return 0; } int clampedIndex = Mathf.Clamp(PageIndex, 0, TotalPages - 1); return clampedIndex * PageSize; } } /// /// Gets the number of items visible on the current page. /// public int VisibleCount { get { if (TotalItems <= 0 || PageSize <= 0) { return 0; } int clampedIndex = Mathf.Clamp(PageIndex, 0, TotalPages - 1); int start = clampedIndex * PageSize; return Mathf.Clamp(TotalItems - start, 0, PageSize); } } } } #endif }