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