diff --git a/.gitignore b/.gitignore
index 2bbf2b4..c89115e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -46,13 +46,13 @@ ExportedObj/
# Rider Generated
.idea
-# Unity3D generated meta files
+# Unity3D files
*.pidb.meta
*.pdb.meta
*.mdb.meta
-
-# Unity3D generated file on crash reports
sysinfo.txt
+!Samples~/
+!**/Samples~/**
# Builds
*.apk
@@ -61,7 +61,7 @@ sysinfo.txt
# Crashlytics generated file
crashlytics-build.properties
-#OS generated
+# OS generated
**/.DS_Store
**/DS_Store
**/DS_Store?*
@@ -71,11 +71,10 @@ crashlytics-build.properties
**/ehthumbs.db
**/Thumbs.db
-#Windows generated
+# Windows generated
*Thumbs*
*.orig
*.orig.meta
*.uxf
-
diff --git a/CHANGELOG.md b/CHANGELOG.md
index dde79b0..c6face8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,44 @@ All notable changes to this package will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html)
+## [1.0.0] - 2025-11-04
+
+**New**:
+- Added `IUiAnalytics` interface and `UiAnalytics` implementation for performance tracking
+- Added three editor windows: `UiAnalyticsWindow`, `UiServiceHierarchyWindow`
+- Added new "UI Layer Hierarchy Visualizer" section to the `UiConfigsEditor` inspector
+- Added `UiPresenterSceneGizmos` for visual debugging in Scene view
+- Added `UiPresenterEditor` custom inspector with quick open/close buttons
+- Added multi-instance support for UI presenters via `UiInstanceId` struct and instance addresses
+- Added `UiInstance` struct to encapsulate presenter metadata (type, address, presenter reference)
+- Added feature-based presenter composition architecture with `IPresenterFeature` interface
+- Added `PresenterFeatureBase` base class for composable presenter features
+- Added `AnimationDelayFeature` and `TimeDelayFeature` components for delayed UI operations
+- Added `UiToolkitPresenterFeature` for UI Toolkit integration
+- Added `DefaultUiConfigsEditor` for out-of-the-box UI configuration (no custom implementation required)
+
+**Changed**:
+- Replaced `Task.Delay` with `UniTask.Delay` throughout for better performance and WebGL compatibility
+- Updated `CloseAllUi` to avoid modifying collection during iteration
+- Enhanced `UiService.Dispose()` with proper cleanup of all presenters, layers, and asset loader
+- `LoadUiAsync`, `OpenUiAsync` methods now accept optional `CancellationToken` parameter
+- Updated the README with a complete information of the project
+- Replaced `LoadedPresenters` property with `GetLoadedPresenters()` method for better encapsulation
+- Migrated all delay functionality from `PresenterDelayerBase` to feature-based system (`AnimationDelayFeature`, `TimeDelayFeature`)
+- Converted all editor scripts to use UI Toolkit for better performance and modern UI
+- Refactored `UiConfigsEditor` to use UI Toolkit with improved visuals and drag-and-drop support
+- Optimized collection types (`Dictionary`, `List`) for better performance in `UiService`
+- Removed loading spinner from `UiService` (simplified initialization)
+
+**Fixed**:
+- **CRITICAL**: Fixed `GetOrLoadUiAsync` returning null when loading new UI (now properly assigns return value)
+- Fixed exception handling in `UnloadUi` with proper `TryGetValue` checks
+- Fixed exception handling in `RemoveUiSet` with proper `TryGetValue` checks
+- Fixed redundant operations in `CloseAllUi` logic
+- Fixed initial value handling for UI sets in editor
+- Fixed serialization updates before property binding in editor
+- Fixed script indentation issues in delay presenter implementations
+
## [0.13.1] - 2025-09-28
**New**:
diff --git a/Editor/DefaultUiConfigsEditor.cs b/Editor/DefaultUiConfigsEditor.cs
new file mode 100644
index 0000000..4029908
--- /dev/null
+++ b/Editor/DefaultUiConfigsEditor.cs
@@ -0,0 +1,30 @@
+using GameLovers.UiService;
+using UnityEditor;
+
+namespace GameLoversEditor.UiService
+{
+ ///
+ /// Default UI Set identifiers for out-of-the-box usage.
+ /// Users can create their own enum and custom editor to override these defaults.
+ ///
+ public enum DefaultUiSetId
+ {
+ InitialLoading = 0,
+ MainMenu = 1,
+ Gameplay = 2,
+ Settings = 3,
+ Overlays = 4,
+ Popups = 5
+ }
+
+ ///
+ /// Default implementation of the UiConfigs editor.
+ /// This allows the library to work out-of-the-box without requiring user implementation.
+ /// Users can override by creating their own CustomEditor implementation for UiConfigs.
+ ///
+ [CustomEditor(typeof(UiConfigs))]
+ public class DefaultUiConfigsEditor : UiConfigsEditor
+ {
+ // No additional implementation needed - uses base class functionality
+ }
+}
\ No newline at end of file
diff --git a/Editor/DefaultUiConfigsEditor.cs.meta b/Editor/DefaultUiConfigsEditor.cs.meta
new file mode 100644
index 0000000..5a2689d
--- /dev/null
+++ b/Editor/DefaultUiConfigsEditor.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 59b150f8c9ed42b6b3113f2ff0c7b63b
+timeCreated: 1762120608
\ No newline at end of file
diff --git a/Editor/NonDrawingViewEditor.cs b/Editor/NonDrawingViewEditor.cs
index a5a5f35..96b1a68 100644
--- a/Editor/NonDrawingViewEditor.cs
+++ b/Editor/NonDrawingViewEditor.cs
@@ -1,7 +1,8 @@
using GameLovers.UiService.Views;
using UnityEditor;
using UnityEditor.UI;
-using UnityEngine;
+using UnityEditor.UIElements;
+using UnityEngine.UIElements;
// ReSharper disable once CheckNamespace
@@ -13,14 +14,25 @@ namespace GameLoversEditor.UiService
[CanEditMultipleObjects, CustomEditor(typeof(NonDrawingView), false)]
public class NonDrawingViewEditor : GraphicEditor
{
- public override void OnInspectorGUI ()
+ public override VisualElement CreateInspectorGUI()
{
- serializedObject.Update();
- EditorGUILayout.PropertyField(m_Script, new GUILayoutOption[0]);
-
- // skipping AppearanceControlsGUI
- RaycastControlsGUI();
- serializedObject.ApplyModifiedProperties();
+ var root = new VisualElement();
+
+ // Add script field
+ var scriptField = new PropertyField(serializedObject.FindProperty("m_Script"));
+ scriptField.SetEnabled(false);
+ root.Add(scriptField);
+
+ // Add raycast controls using IMGUI container since it's from base class
+ var raycastContainer = new IMGUIContainer(() =>
+ {
+ serializedObject.Update();
+ RaycastControlsGUI();
+ serializedObject.ApplyModifiedProperties();
+ });
+ root.Add(raycastContainer);
+
+ return root;
}
}
}
\ No newline at end of file
diff --git a/Editor/UiAnalyticsWindow.cs b/Editor/UiAnalyticsWindow.cs
new file mode 100644
index 0000000..35af5a1
--- /dev/null
+++ b/Editor/UiAnalyticsWindow.cs
@@ -0,0 +1,338 @@
+using System.Linq;
+using UnityEditor;
+using UnityEngine;
+using UnityEngine.UIElements;
+using GameLovers.UiService;
+
+// ReSharper disable once CheckNamespace
+
+namespace GameLoversEditor.UiService
+{
+ ///
+ /// Editor window for viewing UI analytics and performance metrics.
+ /// This window uses UiService.CurrentAnalytics (internal) to access the analytics instance
+ /// from the currently active UiService in play mode.
+ /// Note: CurrentAnalytics is internal and only accessible to editor code within this package.
+ ///
+ public class UiAnalyticsWindow : EditorWindow
+ {
+ private bool _autoRefresh = true;
+ private double _lastRefreshTime;
+ private const double RefreshInterval = 1.0; // seconds
+
+ private ScrollView _scrollView;
+ private VisualElement _contentContainer;
+ private Label _footerStats;
+
+ [MenuItem("Tools/UI Service/Analytics")]
+ public static void ShowWindow()
+ {
+ var window = GetWindow("UI Analytics");
+ window.minSize = new Vector2(500, 300);
+ window.Show();
+ }
+
+ // ReSharper disable once UnusedMember.Local
+ private void OnEnable()
+ {
+ EditorApplication.playModeStateChanged += OnPlayModeStateChanged;
+ }
+
+ // ReSharper disable once UnusedMember.Local
+ private void OnDisable()
+ {
+ EditorApplication.playModeStateChanged -= OnPlayModeStateChanged;
+ }
+
+ private void OnPlayModeStateChanged(PlayModeStateChange state)
+ {
+ if (rootVisualElement != null)
+ {
+ UpdateContent();
+ }
+ }
+
+ private void CreateGUI()
+ {
+ var root = rootVisualElement;
+ root.Clear();
+
+ // Header
+ var header = CreateHeader();
+ root.Add(header);
+
+ // Content container
+ _contentContainer = new VisualElement();
+ root.Add(_contentContainer);
+
+ // Scroll view
+ _scrollView = new ScrollView();
+ _scrollView.style.flexGrow = 1;
+ _contentContainer.Add(_scrollView);
+
+ // Footer
+ var footer = CreateFooter();
+ root.Add(footer);
+
+ // Update content
+ UpdateContent();
+
+ // Schedule periodic updates
+ root.schedule.Execute(() =>
+ {
+ if (_autoRefresh && Application.isPlaying && EditorApplication.timeSinceStartup - _lastRefreshTime > RefreshInterval)
+ {
+ _lastRefreshTime = EditorApplication.timeSinceStartup;
+ UpdateContent();
+ }
+ }).Every(100);
+ }
+
+ private VisualElement CreateHeader()
+ {
+ var header = new VisualElement();
+ header.style.flexDirection = FlexDirection.Row;
+ header.style.backgroundColor = new Color(0.2f, 0.2f, 0.2f);
+ header.style.paddingTop = 5;
+ header.style.paddingBottom = 5;
+ header.style.paddingLeft = 5;
+ header.style.paddingRight = 5;
+ header.style.marginBottom = 5;
+
+ var titleLabel = new Label("UI Service Analytics");
+ titleLabel.style.unityFontStyleAndWeight = FontStyle.Bold;
+ titleLabel.style.flexGrow = 1;
+ header.Add(titleLabel);
+
+ // Auto refresh toggle
+ var autoRefreshToggle = new Toggle("Auto Refresh") { value = _autoRefresh };
+ autoRefreshToggle.RegisterValueChangedCallback(evt => _autoRefresh = evt.newValue);
+ autoRefreshToggle.style.width = 110;
+ header.Add(autoRefreshToggle);
+
+ // Refresh button
+ var refreshButton = new Button(() => UpdateContent()) { text = "Refresh" };
+ refreshButton.style.width = 60;
+ refreshButton.style.marginLeft = 5;
+ header.Add(refreshButton);
+
+ // Clear button
+ var clearButton = new Button(() => GetCurrentAnalytics()?.Clear()) { text = "Clear" };
+ clearButton.style.width = 50;
+ clearButton.style.marginLeft = 5;
+ header.Add(clearButton);
+
+ return header;
+ }
+
+ private void UpdateContent()
+ {
+ if (_scrollView == null)
+ return;
+
+ _scrollView.Clear();
+
+ if (!Application.isPlaying)
+ {
+ var helpBox = new HelpBox(
+ "UI Analytics is only available in Play Mode.\n\n" +
+ "Enter Play Mode to see performance metrics and event tracking.",
+ HelpBoxMessageType.Info);
+ _scrollView.Add(helpBox);
+ UpdateFooter();
+ return;
+ }
+
+ var analytics = GetCurrentAnalytics();
+
+ if (analytics == null)
+ {
+ var warningBox = new HelpBox("No UiService instance found. (NullAnalytics is used by default)\n" +
+ "Create a UiService with UiAnalytics to enable analytics tracking. (e.g. var uiService = new UiService(new UiAssetLoader(), new UiAnalytics());)", HelpBoxMessageType.Warning);
+ _scrollView.Add(warningBox);
+ UpdateFooter();
+ return;
+ }
+
+ var metrics = analytics.PerformanceMetrics;
+
+ if (metrics.Count == 0)
+ {
+ var infoBox = new HelpBox("No analytics data collected yet. Use the UI Service to generate data.", HelpBoxMessageType.Info);
+ _scrollView.Add(infoBox);
+ UpdateFooter();
+ return;
+ }
+
+ var trackedLabel = new Label($"Tracked UIs: {metrics.Count}");
+ trackedLabel.style.unityFontStyleAndWeight = FontStyle.Bold;
+ trackedLabel.style.marginBottom = 5;
+ trackedLabel.style.marginLeft = 5;
+ _scrollView.Add(trackedLabel);
+
+ // Sort by total lifetime
+ var sortedMetrics = metrics.Values.OrderByDescending(m => m.TotalLifetime).ToList();
+
+ foreach (var metric in sortedMetrics)
+ {
+ var card = CreateMetricCard(metric);
+ _scrollView.Add(card);
+ }
+
+ UpdateFooter();
+ }
+
+ private VisualElement CreateMetricCard(UiPerformanceMetrics metric)
+ {
+ var card = new VisualElement();
+ card.style.backgroundColor = new Color(0.2f, 0.2f, 0.2f, 0.3f);
+ card.style.borderTopLeftRadius = 4;
+ card.style.borderTopRightRadius = 4;
+ card.style.borderBottomLeftRadius = 4;
+ card.style.borderBottomRightRadius = 4;
+ card.style.marginLeft = 5;
+ card.style.marginRight = 5;
+ card.style.marginBottom = 5;
+ card.style.paddingTop = 8;
+ card.style.paddingBottom = 8;
+ card.style.paddingLeft = 10;
+ card.style.paddingRight = 10;
+
+ // Header
+ var header = new Label(metric.UiName);
+ header.style.unityFontStyleAndWeight = FontStyle.Bold;
+ header.style.fontSize = 13;
+ header.style.marginBottom = 8;
+ card.Add(header);
+
+ // Performance metrics
+ card.Add(CreateMetricRow("Load Duration:", $"{metric.LoadDuration:F3}s", GetLoadColor(metric.LoadDuration)));
+ card.Add(CreateMetricRow("Open Duration:", $"{metric.OpenDuration:F3}s", GetOpenColor(metric.OpenDuration)));
+ card.Add(CreateMetricRow("Close Duration:", $"{metric.CloseDuration:F3}s", GetCloseColor(metric.CloseDuration)));
+
+ card.Add(CreateSpacer(5));
+
+ card.Add(CreateMetricRow("Open Count:", metric.OpenCount.ToString(), Color.white));
+ card.Add(CreateMetricRow("Close Count:", metric.CloseCount.ToString(), Color.white));
+ card.Add(CreateMetricRow("Total Lifetime:", $"{metric.TotalLifetime:F1}s", Color.cyan));
+
+ if (metric.FirstOpened != System.DateTime.MinValue)
+ {
+ card.Add(CreateSpacer(5));
+ card.Add(CreateMetricRow("First Opened:", metric.FirstOpened.ToString("HH:mm:ss"), Color.white));
+ }
+
+ if (metric.LastClosed != System.DateTime.MinValue)
+ {
+ card.Add(CreateMetricRow("Last Closed:", metric.LastClosed.ToString("HH:mm:ss"), Color.white));
+ }
+
+ return card;
+ }
+
+ private VisualElement CreateMetricRow(string label, string value, Color color)
+ {
+ var row = new VisualElement();
+ row.style.flexDirection = FlexDirection.Row;
+ row.style.marginBottom = 2;
+
+ var labelElement = new Label(label);
+ labelElement.style.width = 150;
+ labelElement.style.marginLeft = 10;
+ row.Add(labelElement);
+
+ var valueElement = new Label(value);
+ valueElement.style.unityFontStyleAndWeight = FontStyle.Bold;
+ valueElement.style.color = color;
+ row.Add(valueElement);
+
+ return row;
+ }
+
+ private VisualElement CreateFooter()
+ {
+ var footer = new VisualElement();
+ footer.style.flexDirection = FlexDirection.Row;
+ footer.style.backgroundColor = new Color(0.2f, 0.2f, 0.2f);
+ footer.style.paddingTop = 5;
+ footer.style.paddingBottom = 5;
+ footer.style.paddingLeft = 5;
+ footer.style.paddingRight = 5;
+ footer.style.marginTop = 5;
+
+ _footerStats = new Label();
+ _footerStats.style.flexGrow = 1;
+ footer.Add(_footerStats);
+
+ var logButton = new Button(() => GetCurrentAnalytics()?.LogPerformanceSummary()) { text = "Log Summary" };
+ logButton.style.width = 100;
+ footer.Add(logButton);
+
+ return footer;
+ }
+
+ private void UpdateFooter()
+ {
+ if (_footerStats == null)
+ return;
+
+ var analytics = GetCurrentAnalytics();
+ if (analytics != null)
+ {
+ var metrics = analytics.PerformanceMetrics;
+ if (metrics.Count > 0)
+ {
+ var totalOpens = metrics.Values.Sum(m => m.OpenCount);
+ var totalCloses = metrics.Values.Sum(m => m.CloseCount);
+ _footerStats.text = $"Total Opens: {totalOpens} | Total Closes: {totalCloses}";
+ }
+ else
+ {
+ _footerStats.text = "";
+ }
+ }
+ else
+ {
+ _footerStats.text = "";
+ }
+ }
+
+ private VisualElement CreateSpacer(int height)
+ {
+ var spacer = new VisualElement();
+ spacer.style.height = height;
+ return spacer;
+ }
+
+ ///
+ /// Helper method to get the current analytics instance from the active UiService.
+ /// Note: This accesses an internal property only available to editor code within this package.
+ ///
+ private IUiAnalytics GetCurrentAnalytics()
+ {
+ return GameLovers.UiService.UiService.CurrentAnalytics;
+ }
+
+ private Color GetLoadColor(float duration)
+ {
+ if (duration < 0.1f) return Color.green;
+ if (duration < 0.5f) return Color.yellow;
+ return Color.red;
+ }
+
+ private Color GetOpenColor(float duration)
+ {
+ if (duration < 0.05f) return Color.green;
+ if (duration < 0.2f) return Color.yellow;
+ return Color.red;
+ }
+
+ private Color GetCloseColor(float duration)
+ {
+ if (duration < 0.05f) return Color.green;
+ if (duration < 0.2f) return Color.yellow;
+ return Color.red;
+ }
+ }
+}
+
diff --git a/Editor/UiAnalyticsWindow.cs.meta b/Editor/UiAnalyticsWindow.cs.meta
new file mode 100644
index 0000000..c14bf6f
--- /dev/null
+++ b/Editor/UiAnalyticsWindow.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 1997986c632194915931f1948be974f5
\ No newline at end of file
diff --git a/Editor/UiConfigsEditor.cs b/Editor/UiConfigsEditor.cs
index 95ba248..d985e9f 100644
--- a/Editor/UiConfigsEditor.cs
+++ b/Editor/UiConfigsEditor.cs
@@ -1,10 +1,11 @@
using System;
using System.Collections.Generic;
+using System.Linq;
using GameLovers.UiService;
using UnityEditor;
using UnityEditor.AddressableAssets;
using UnityEditor.AddressableAssets.Settings;
-using UnityEditorInternal;
+using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;
@@ -15,10 +16,9 @@ namespace GameLoversEditor.UiService
///
/// Helps selecting the asset file in the Editor
///
- public static class UiConfigsSelect
+ public static class UiConfigsMenuItems
{
-
- [MenuItem("Tools/Select UiConfigs.asset")]
+ [MenuItem("Tools/UI Service/Select UiConfigs")]
private static void SelectUiConfigs()
{
var assets = AssetDatabase.FindAssets($"t:{nameof(UiConfigs)}");
@@ -34,9 +34,33 @@ private static void SelectUiConfigs()
}
Selection.activeObject = scriptableObject;
+ FocusInspectorWindow();
+ }
+
+ [MenuItem("Tools/UI Service/Layer Visualizer")]
+ public static void ShowLayerVisualizer()
+ {
+ // Set the pref BEFORE selecting so OnEnable reads the correct value
+ EditorPrefs.SetBool("UiConfigsEditor_ShowVisualizer", true);
+
+ SelectUiConfigs();
+
+ // Force inspector refresh to rebuild the UI
+ ActiveEditorTracker.sharedTracker.ForceRebuild();
+ }
+
+ private static void FocusInspectorWindow()
+ {
+ // Get the Inspector window type using reflection
+ var inspectorType = typeof(Editor).Assembly.GetType("UnityEditor.InspectorWindow");
+ if (inspectorType != null)
+ {
+ // Focus or create the Inspector window
+ EditorWindow.GetWindow(inspectorType);
+ }
}
}
-
+
///
/// Improves the inspector visualization for the scriptable object
///
@@ -53,247 +77,862 @@ private static void SelectUiConfigs()
public abstract class UiConfigsEditor : Editor
where TSet : Enum
{
- private readonly List _assetsPath = new List();
- private readonly List _uiConfigsType = new List();
- private readonly List _setsConfigsList = new List();
- private readonly GUIContent _uiConfigGuiContent = new GUIContent("Ui Config",
- "All the Addressable addresses for every UiPresenter in the game.\n" +
- "The second field is the layer where the UiPresenter should be shown. " +
- "The higher the value, the closer is the UiPresenter to the camera.\n" +
- "If the UiPresenter contains a Canvas/UIDocument in the root, the layer value is the same of the UI sorting order");
- private readonly GUIContent _uiSetConfigGuiContent = new GUIContent("Ui Set",
- "All the Ui Sets in the game.\n" +
- "A UiSet groups a list of UiConfigs and shows them all at the same time via the UiService.\n" +
- "The UiConfigs are all loaded in the order they are configured. Top = first; Bottom = Last");
-
- private string[] _uiConfigsAddress;
+ private const string UiConfigExplanation =
+ "UI Presenter Configurations\n\n" +
+ "Lists all Addressable UI Presenter prefabs in the game with their sorting layer values. " +
+ "The Layer field controls the rendering order - higher values appear closer to the camera. " +
+ "For presenters with Canvas or UIDocument components, this value directly maps to the UI sorting order.";
+
+ private const string UiSetExplanation =
+ "UI Set Configurations\n\n" +
+ "UI Sets group multiple presenter instances that should be displayed together. " +
+ "When a set is activated via UiService, all its presenters are loaded and shown simultaneously. " +
+ "Presenters are loaded in the order listed (top to bottom).\n\n" +
+ "Each UI's instance address is automatically set to its Addressable address from the config.";
+
+ private const string VisualizerPrefsKey = "UiConfigsEditor_ShowVisualizer";
+
+ private Dictionary _assetPathLookup;
+ private List _uiConfigsAddress;
+ private Dictionary _uiTypesByAddress; // Maps addressable address to Type
+ private UiConfigs _scriptableObject;
private SerializedProperty _configsProperty;
private SerializedProperty _setsProperty;
- private ReorderableList _configList;
- private ReorderableList _setList;
- private bool _resetValues;
- private UiConfigs _scriptableObject;
+ private bool _showVisualizer;
+ private VisualElement _visualizerContainer;
+ private string _visualizerSearchFilter = "";
private void OnEnable()
{
- _resetValues = false;
-
- InitConfigValues();
- InitReorderableLists();
+ _scriptableObject = target as UiConfigs;
+ if (_scriptableObject == null)
+ {
+ return;
+ }
- _setList.drawHeaderCallback = rect => EditorGUI.LabelField(rect, $"Ui {nameof(UiConfigs.Sets)}");
- _setList.elementHeightCallback = index => _setsConfigsList[index].GetHeight();
- _setList.drawElementCallback = (rect, index, active, focused) => _setsConfigsList[index].DoList(rect);
- _configList.drawHeaderCallback = rect => EditorGUI.LabelField(rect, $"Ui {nameof(UiConfigs.Configs)}");
- _configList.drawElementCallback = DrawUiConfigElement;
+ SyncConfigsWithAddressables();
+
+ // Ensure sets array matches enum size
+ _scriptableObject.SetSetsSize(Enum.GetNames(typeof(TSet)).Length);
+
+ // Update the serializedObject to reflect the changes
+ serializedObject.Update();
+
+ _configsProperty = serializedObject.FindProperty("_configs");
+ _setsProperty = serializedObject.FindProperty("_sets");
+
+ // Load visualizer visibility state
+ _showVisualizer = EditorPrefs.GetBool(VisualizerPrefsKey, false);
+ }
+
+ ///
+ /// Public method to show the visualizer, used by menu items
+ ///
+ public static void ShowVisualizerForConfigs(UiConfigs configs)
+ {
+ if (configs == null) return;
+
+ Selection.activeObject = configs;
+ EditorPrefs.SetBool(VisualizerPrefsKey, true);
}
///
- public override void OnInspectorGUI()
+ public override VisualElement CreateInspectorGUI()
{
- serializedObject.Update();
+ var root = new VisualElement();
+ root.style.paddingTop = 5;
+ root.style.paddingBottom = 5;
+ root.style.paddingLeft = 3;
+ root.style.paddingRight = 3;
+
+ // Section 0: Layer Visualizer (collapsible)
+ var visualizerSection = CreateVisualizerSection();
+ root.Add(visualizerSection);
+
+ // Section 1: UI Config Explanation
+ var configHelpBox = new HelpBox(UiConfigExplanation, HelpBoxMessageType.Info);
+ configHelpBox.style.marginBottom = 10;
+ root.Add(configHelpBox);
+
+ // Section 2: UI Configs List
+ var configsListView = CreateConfigsListView();
+ root.Add(configsListView);
+
+ // Section 3: Separator
+ var separator = new VisualElement();
+ separator.style.height = 1;
+ separator.style.backgroundColor = new Color(0.5f, 0.5f, 0.5f, 0.5f);
+ separator.style.marginTop = 15;
+ separator.style.marginBottom = 15;
+ root.Add(separator);
+
+ // Section 4: UI Set Explanation
+ var setHelpBox = new HelpBox(UiSetExplanation, HelpBoxMessageType.Info);
+ setHelpBox.style.marginBottom = 10;
+ root.Add(setHelpBox);
+
+ // Section 5: UI Sets List
+ var setsContainer = CreateSetsContainer();
+ root.Add(setsContainer);
+
+ return root;
+ }
- LoadingSpinnerLayout();
+ private ListView CreateConfigsListView()
+ {
+ var listView = new ListView
+ {
+ showBorder = true,
+ showFoldoutHeader = true,
+ headerTitle = "UI Presenter Configs",
+ showAddRemoveFooter = false,
+ showBoundCollectionSize = false,
+ reorderable = false,
+ virtualizationMethod = CollectionVirtualizationMethod.DynamicHeight,
+ fixedItemHeight = 22
+ };
- EditorGUILayout.Space();
- EditorGUILayout.HelpBox(_uiConfigGuiContent.tooltip, MessageType.Info);
- _configList.DoLayoutList();
- EditorGUILayout.LabelField("", GUI.skin.horizontalSlider);
- EditorGUILayout.Space();
- EditorGUILayout.HelpBox(_uiSetConfigGuiContent.tooltip, MessageType.Info);
- _setList.DoLayoutList();
+ listView.style.minHeight = 20;
+ listView.style.marginBottom = 5;
- serializedObject.ApplyModifiedProperties();
+ listView.BindProperty(_configsProperty);
- if (_resetValues)
- {
- OnEnable();
- }
+ listView.makeItem = CreateConfigElement;
+ listView.bindItem = BindConfigElement;
+
+ return listView;
}
- private void LoadingSpinnerLayout()
+ private void BindConfigElement(VisualElement element, int index)
{
- var uiPresentersNames = new List { "" };
- var uiPresentersAssemblyNames = new List { "" };
+ if (index >= _configsProperty.arraySize)
+ return;
- foreach (var uiConfig in _scriptableObject.Configs)
- {
- uiPresentersNames.Add(uiConfig.UiType.Name);
- uiPresentersAssemblyNames.Add(uiConfig.UiType.AssemblyQualifiedName);
- }
+ var itemProperty = _configsProperty.GetArrayElementAtIndex(index);
+ var addressProperty = itemProperty.FindPropertyRelative(nameof(UiConfigs.UiConfigSerializable.AddressableAddress));
+ var layerProperty = itemProperty.FindPropertyRelative(nameof(UiConfigs.UiConfigSerializable.Layer));
+
+ var label = element.Q