From 9afec7f83642d76e8d5387ef0309d6f840215714 Mon Sep 17 00:00:00 2001 From: haywirephoenix Date: Fri, 29 Nov 2024 13:35:07 +0000 Subject: [PATCH 1/4] TabGroup active tab persistence --- Editor/Elements/TriTabGroupElement.cs | 83 +++++++++++++++++++++++---- 1 file changed, 72 insertions(+), 11 deletions(-) diff --git a/Editor/Elements/TriTabGroupElement.cs b/Editor/Elements/TriTabGroupElement.cs index e28c69a..195033d 100644 --- a/Editor/Elements/TriTabGroupElement.cs +++ b/Editor/Elements/TriTabGroupElement.cs @@ -1,17 +1,19 @@ using System.Collections.Generic; using TriInspector.Resolvers; using UnityEngine; +using UnityEditor; namespace TriInspector.Elements { public class TriTabGroupElement : TriHeaderGroupBaseElement { private const string DefaultTabName = "Main"; - private readonly List _tabs; private readonly Dictionary _tabElements; - private string _activeTabName; + private string _tabGroupId; + private bool _isInitializing; + private bool _activeTabLoaded = false; private struct TabInfo { @@ -25,6 +27,54 @@ public TriTabGroupElement() _tabs = new List(); _tabElements = new Dictionary(); _activeTabName = null; + _tabGroupId = ""; + } + + private string GetTabGroupPreferenceKey() + { + if (string.IsNullOrEmpty(_tabGroupId) && _tabs.Count > 0 && _tabs[0].property != null) + { + // Create a unique key by walking up to find the root object + var property = _tabs[0].property; + var rootProperty = property; + while (rootProperty.Parent != null) + { + rootProperty = rootProperty.Parent; + } + + var targetObject = rootProperty.Value as Object; + if (targetObject != null) + { + _tabGroupId = $"TriTabGroup_{targetObject.GetInstanceID()}_{property.PropertyPath}"; + } + } + return _tabGroupId; + } + + private void SaveActiveTab() + { + if (_isInitializing) return; + + var preferenceKey = GetTabGroupPreferenceKey(); + if (!string.IsNullOrEmpty(preferenceKey) && !string.IsNullOrEmpty(_activeTabName)) + { + EditorPrefs.SetString(preferenceKey, _activeTabName); + } + } + + private bool LoadActiveTab() + { + var preferenceKey = GetTabGroupPreferenceKey(); + if (!string.IsNullOrEmpty(preferenceKey)) + { + var savedTab = EditorPrefs.GetString(preferenceKey, null); + if (!string.IsNullOrEmpty(savedTab) && _tabElements.ContainsKey(savedTab)) + { + SetActiveTabInternal(savedTab); + return true; + } + } + return false; } protected override void DrawHeader(Rect position) @@ -34,6 +84,13 @@ protected override void DrawHeader(Rect position) return; } + // Load saved tab state if no active tab is set + if (!_activeTabLoaded) + { + _activeTabLoaded = true; + LoadActiveTab(); + } + var tabRect = new Rect(position) { width = position.width / _tabs.Count, @@ -54,13 +111,11 @@ protected override void DrawHeader(Rect position) var tabStyle = index == 0 ? TriEditorStyles.TabFirst : index == tabCount - 1 ? TriEditorStyles.TabLast : TriEditorStyles.TabMiddle; - var isTabActive = GUI.Toggle(tabRect, _activeTabName == tab.name, content, tabStyle); if (isTabActive && _activeTabName != tab.name) { SetActiveTab(tab.name); } - tabRect.x += tabRect.width; } } @@ -69,7 +124,6 @@ protected override void DrawHeader(Rect position) protected override void AddPropertyChild(TriElement element, TriProperty property) { var tabName = DefaultTabName; - if (property.TryGetAttribute(out TabAttribute tab)) { tabName = tab.TabName ?? tabName; @@ -78,14 +132,12 @@ protected override void AddPropertyChild(TriElement element, TriProperty propert if (!_tabElements.TryGetValue(tabName, out var tabElement)) { tabElement = new TriElement(); - var info = new TabInfo { name = tabName, titleResolver = ValueResolver.ResolveString(property.Definition, tabName), property = property, }; - _tabElements[tabName] = tabElement; _tabs.Add(info); @@ -96,20 +148,29 @@ protected override void AddPropertyChild(TriElement element, TriProperty propert if (_activeTabName == null) { - SetActiveTab(tabName); + _isInitializing = true; + if (!LoadActiveTab()) + { + SetActiveTabInternal(tabName); + } + _isInitializing = false; } } tabElement.AddChild(element); } - private void SetActiveTab(string tabName) + private void SetActiveTabInternal(string tabName) { _activeTabName = tabName; - RemoveAllChildren(); - AddChild(_tabElements[_activeTabName]); } + + private void SetActiveTab(string tabName) + { + SetActiveTabInternal(tabName); + SaveActiveTab(); + } } } \ No newline at end of file From a27e3ca4edf0d9014bdbaea6d5aebd1810296e06 Mon Sep 17 00:00:00 2001 From: haywire Date: Sun, 10 Aug 2025 11:49:50 +0100 Subject: [PATCH 2/4] add dynamic tab area height for better resizing, improve tab performance --- Editor/Elements/TriTabGroupElement.cs | 313 +++++++++++++++++++++++++- 1 file changed, 301 insertions(+), 12 deletions(-) diff --git a/Editor/Elements/TriTabGroupElement.cs b/Editor/Elements/TriTabGroupElement.cs index 195033d..9bd668c 100644 --- a/Editor/Elements/TriTabGroupElement.cs +++ b/Editor/Elements/TriTabGroupElement.cs @@ -8,12 +8,34 @@ namespace TriInspector.Elements public class TriTabGroupElement : TriHeaderGroupBaseElement { private const string DefaultTabName = "Main"; + const float minTabWidth = 80f; + const float tabHeight = 20f; private readonly List _tabs; private readonly Dictionary _tabElements; private string _activeTabName; private string _tabGroupId; private bool _isInitializing; private bool _activeTabLoaded = false; + + // Enhanced caching for performance + private float[] _cachedTabWidths; + private List> _cachedRows; + private float _cachedLayoutWidth = -1f; + private float _cachedLayoutHeight = -1f; + private bool _tabContentDirty = true; + + // Reusable objects to avoid allocations + private readonly List _reusableRowList = new List(); + private readonly GUIContent _reusableGUIContent = new GUIContent(); + + // Throttling for resize events + private float _lastWidthChangeTime; + private const float WidthChangeThrottleTime = 0.016f; // ~60fps throttling + private float _pendingWidth = -1f; + + // Significant width change thresholds + private const float MinWidthChangeThreshold = 5f; // Minimum pixels to trigger recalc + private const float RelativeWidthChangeThreshold = 0.02f; // 2% relative change private struct TabInfo { @@ -28,13 +50,14 @@ public TriTabGroupElement() _tabElements = new Dictionary(); _activeTabName = null; _tabGroupId = ""; + _cachedTabWidths = null; + _cachedRows = null; } private string GetTabGroupPreferenceKey() { if (string.IsNullOrEmpty(_tabGroupId) && _tabs.Count > 0 && _tabs[0].property != null) { - // Create a unique key by walking up to find the root object var property = _tabs[0].property; var rootProperty = property; while (rootProperty.Parent != null) @@ -77,6 +100,220 @@ private bool LoadActiveTab() return false; } + private void InvalidateLayoutCache() + { + _tabContentDirty = true; + _cachedLayoutWidth = -1f; + _cachedLayoutHeight = -1f; + _cachedTabWidths = null; + + // Don't immediately clear _cachedRows - let it be reused if possible + if (_cachedRows != null) + { + // Clear existing rows but keep the list structure for reuse + foreach (var row in _cachedRows) + { + row.Clear(); + } + } + } + + private bool NeedsTabContentRecalculation() + { + if (_tabContentDirty) + return true; + + // Check if any tab titles have changed (for dynamic titles) + if (_cachedTabWidths != null && _cachedTabWidths.Length == _tabs.Count) + { + for (int i = 0; i < _tabs.Count; i++) + { + var content = _tabs[i].titleResolver.GetValue(_tabs[i].property); + + // Reuse GUIContent object to avoid allocation + _reusableGUIContent.text = content; + var contentSize = GUI.skin.button.CalcSize(_reusableGUIContent); + var expectedWidth = Mathf.Max(contentSize.x + 20f, minTabWidth); + + if (Mathf.Abs(_cachedTabWidths[i] - expectedWidth) > 0.1f) + { + return true; + } + } + } + + return false; + } + + private void UpdateTabWidthsIfNeeded() + { + if (!NeedsTabContentRecalculation()) + return; + + // Reuse array if possible + if (_cachedTabWidths == null || _cachedTabWidths.Length != _tabs.Count) + { + _cachedTabWidths = new float[_tabs.Count]; + } + + for (int i = 0; i < _tabs.Count; i++) + { + var content = _tabs[i].titleResolver.GetValue(_tabs[i].property); + + // Reuse GUIContent object to avoid allocation + _reusableGUIContent.text = content; + var contentSize = GUI.skin.button.CalcSize(_reusableGUIContent); + _cachedTabWidths[i] = Mathf.Max(contentSize.x + 20f, minTabWidth); + } + + _tabContentDirty = false; + // Invalidate layout cache since tab widths changed + _cachedLayoutWidth = -1f; + + // Clear rows but don't deallocate + if (_cachedRows != null) + { + foreach (var row in _cachedRows) + { + row.Clear(); + } + _cachedRows.Clear(); + } + } + + private bool IsWidthChangeSignificant(float newWidth, float cachedWidth) + { + if (cachedWidth < 0) return true; // No cached width + + var absoluteDiff = Mathf.Abs(newWidth - cachedWidth); + + // Must exceed minimum pixel threshold + if (absoluteDiff < MinWidthChangeThreshold) return false; + + // AND must exceed relative threshold for larger widths + var relativeDiff = absoluteDiff / Mathf.Max(cachedWidth, 1f); + return relativeDiff >= RelativeWidthChangeThreshold; + } + + private List> GetRowsForWidth(float availableWidth) + { + // Throttle width changes to avoid excessive recalculations + if (_pendingWidth != availableWidth) + { + _pendingWidth = availableWidth; + _lastWidthChangeTime = Time.realtimeSinceStartup; + } + + // If we're in a throttle period and have cached data, use it + if (Time.realtimeSinceStartup - _lastWidthChangeTime < WidthChangeThrottleTime && + _cachedRows != null && + !IsWidthChangeSignificant(availableWidth, _cachedLayoutWidth)) + { + return _cachedRows; + } + + UpdateTabWidthsIfNeeded(); + + // Return cached layout if width hasn't changed significantly + if (!IsWidthChangeSignificant(availableWidth, _cachedLayoutWidth) && + _cachedRows != null && _cachedRows.Count > 0) + { + return _cachedRows; + } + + // Initialize or reuse the cached rows structure + if (_cachedRows == null) + { + _cachedRows = new List>(); + } + else + { + // Clear existing rows but reuse the lists + foreach (var row in _cachedRows) + { + row.Clear(); + } + _cachedRows.Clear(); + } + + // Reuse the temporary row list + _reusableRowList.Clear(); + var currentRowWidth = 0f; + var availableRowsIndex = 0; + + for (int i = 0; i < _tabs.Count; i++) + { + if (currentRowWidth + _cachedTabWidths[i] > availableWidth && _reusableRowList.Count > 0) + { + // Reuse existing row list if available, otherwise create new + List rowToAdd; + if (availableRowsIndex < _cachedRows.Count) + { + rowToAdd = _cachedRows[availableRowsIndex]; + } + else + { + rowToAdd = new List(); + _cachedRows.Add(rowToAdd); + } + + // Copy items from reusable list + rowToAdd.AddRange(_reusableRowList); + availableRowsIndex++; + + _reusableRowList.Clear(); + currentRowWidth = 0f; + } + + _reusableRowList.Add(i); + currentRowWidth += _cachedTabWidths[i]; + } + + if (_reusableRowList.Count > 0) + { + List finalRow; + if (availableRowsIndex < _cachedRows.Count) + { + finalRow = _cachedRows[availableRowsIndex]; + } + else + { + finalRow = new List(); + _cachedRows.Add(finalRow); + } + + finalRow.AddRange(_reusableRowList); + } + + _cachedLayoutWidth = availableWidth; + return _cachedRows; + } + + private (float[] tabWidths, List> rows) CalculateTabLayout(float availableWidth) + { + var rows = GetRowsForWidth(availableWidth); + return (_cachedTabWidths, rows); + } + + protected override float GetHeaderHeight(float width) + { + if (_tabs.Count <= 1) + { + return 20f; // Single row height + } + + // Return cached height if width hasn't changed significantly + if (!IsWidthChangeSignificant(width, _cachedLayoutWidth) && _cachedLayoutHeight > 0f) + { + return _cachedLayoutHeight; + } + + var rows = GetRowsForWidth(width); + _cachedLayoutHeight = rows.Count * tabHeight; + + return _cachedLayoutHeight; + } + protected override void DrawHeader(Rect position) { if (_tabs.Count == 0) @@ -91,33 +328,82 @@ protected override void DrawHeader(Rect position) LoadActiveTab(); } - var tabRect = new Rect(position) - { - width = position.width / _tabs.Count, - }; - if (_tabs.Count == 1) { var tab = _tabs[0]; var content = tab.titleResolver.GetValue(tab.property); + var tabRect = new Rect(position) { width = position.width }; GUI.Toggle(tabRect, true, content, TriEditorStyles.TabOnlyOne); } else { - for (int index = 0, tabCount = _tabs.Count; index < tabCount; index++) + DrawMultiRowTabs(position); + } + } + + private void DrawMultiRowTabs(Rect position) + { + var (tabWidths, rows) = CalculateTabLayout(position.width); + var availableWidth = position.width; + + // Draw tabs row by row + var currentY = position.y; + + for (int rowIndex = 0; rowIndex < rows.Count; rowIndex++) + { + var row = rows[rowIndex]; + var rowRect = new Rect(position.x, currentY, position.width, tabHeight); + + // Calculate actual widths for tabs in this row (distribute remaining space) + var totalRowWidth = 0f; + foreach (var tabIndex in row) + { + totalRowWidth += tabWidths[tabIndex]; + } + + var extraSpace = Mathf.Max(0, availableWidth - totalRowWidth); + var extraPerTab = row.Count > 0 ? extraSpace / row.Count : 0f; + + var currentX = position.x; + + for (int i = 0; i < row.Count; i++) { - var tab = _tabs[index]; + var tabIndex = row[i]; + var tab = _tabs[tabIndex]; var content = tab.titleResolver.GetValue(tab.property); - var tabStyle = index == 0 ? TriEditorStyles.TabFirst - : index == tabCount - 1 ? TriEditorStyles.TabLast - : TriEditorStyles.TabMiddle; + var tabWidth = tabWidths[tabIndex] + extraPerTab; + + var tabRect = new Rect(currentX, currentY, tabWidth, tabHeight); + + // Determine tab style based on position in the entire tab group + GUIStyle tabStyle; + if (_tabs.Count == 1) + { + tabStyle = TriEditorStyles.TabOnlyOne; + } + else if (tabIndex == 0) + { + tabStyle = TriEditorStyles.TabFirst; + } + else if (tabIndex == _tabs.Count - 1) + { + tabStyle = TriEditorStyles.TabLast; + } + else + { + tabStyle = TriEditorStyles.TabMiddle; + } + var isTabActive = GUI.Toggle(tabRect, _activeTabName == tab.name, content, tabStyle); if (isTabActive && _activeTabName != tab.name) { SetActiveTab(tab.name); } - tabRect.x += tabRect.width; + + currentX += tabWidth; } + + currentY += tabHeight; } } @@ -140,6 +426,9 @@ protected override void AddPropertyChild(TriElement element, TriProperty propert }; _tabElements[tabName] = tabElement; _tabs.Add(info); + + // Mark cache as dirty when tabs are added + InvalidateLayoutCache(); if (info.titleResolver.TryGetErrorString(out var error)) { From 914e28d71496a4cada5c41fa112df948f7ac2272 Mon Sep 17 00:00:00 2001 From: haywire Date: Mon, 18 Aug 2025 22:49:15 +0100 Subject: [PATCH 3/4] minor performance fix --- Editor/Editors/TriEditorCore.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Editor/Editors/TriEditorCore.cs b/Editor/Editors/TriEditorCore.cs index dc7334d..741097c 100644 --- a/Editor/Editors/TriEditorCore.cs +++ b/Editor/Editors/TriEditorCore.cs @@ -46,10 +46,9 @@ public void OnInspectorGUI(VisualElement visualRoot = null) EditorGUILayout.HelpBox("Script is missing", MessageType.Warning); return; } - - foreach (var targetObject in serializedObject.targetObjects) + for(int i = 0; i < serializedObject.targetObjects.Length; i++) { - if (TriGuiHelper.IsEditorTargetPushed(targetObject)) + if (TriGuiHelper.IsEditorTargetPushed(serializedObject.targetObjects[i])) { GUILayout.Label("Recursive inline editors not supported"); return; From c63b766ff8ee5c3959f9c27dfa8d9ee6265f2bc4 Mon Sep 17 00:00:00 2001 From: haywire Date: Mon, 18 Aug 2025 22:51:44 +0100 Subject: [PATCH 4/4] update package version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1ff1732..4a139c0 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "com.codewriter.triinspector", "displayName": "Tri Inspector", "description": "Advanced inspector attributes for Unity", - "version": "1.14.1", + "version": "1.14.2", "unity": "2020.3", "author": "CodeWriter (https://github.com/orgs/codewriter-packages)", "homepage": "https://github.com/codewriter-packages/Tri-Inspector#readme",