diff --git a/.github/instructions/code-review.instructions.md b/.github/instructions/code-review.instructions.md index 0de0e35b..dde750c7 100644 --- a/.github/instructions/code-review.instructions.md +++ b/.github/instructions/code-review.instructions.md @@ -36,6 +36,13 @@ For state accessed across Unity callbacks: - Hot update: `HotUpdate.Code` - **Exception**: `Assets/Scripts/` may contain user-level code without namespace (intentional for user customization) +### 6. Performance Patterns +Avoid LINQ in hot paths and UI code for performance: +- Use `for`/`foreach` loops with inline null checks instead of `.Where()` +- Use `Count > 0` or `Length > 0` instead of `.Any()` +- Use array/list indexing instead of `.First()` / `.Last()` +- LINQ allocates iterators and delegates - avoid in frequently called code + ## Common Issues to Flag - Missing XML documentation on public APIs diff --git a/UnityProject/Packages/com.jasonxudeveloper.jengine.core/Editor/BuildHelper.cs b/UnityProject/Packages/com.jasonxudeveloper.jengine.core/Editor/BuildHelper.cs new file mode 100644 index 00000000..f0497851 --- /dev/null +++ b/UnityProject/Packages/com.jasonxudeveloper.jengine.core/Editor/BuildHelper.cs @@ -0,0 +1,152 @@ +// BuildHelper.cs +// +// Author: +// JasonXuDeveloper +// +// Copyright (c) 2025 JEngine +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +using System; +using JEngine.Core.Editor.CustomEditor; +using UnityEditor; + +namespace JEngine.Core.Editor +{ + /// + /// Helper class for build operations with UI update callbacks. + /// + public static class BuildHelper + { + /// + /// Callbacks for build UI updates. + /// + public class BuildCallbacks + { + /// + /// Called to enable/disable build buttons. + /// + public Action SetButtonsEnabled { get; set; } + + /// + /// Called to clear the log view. + /// + public Action ClearLog { get; set; } + + /// + /// Called to update status text. + /// + public Action UpdateStatus { get; set; } + + /// + /// Called when build completes successfully. + /// + public Action OnSuccess { get; set; } + + /// + /// Called when build fails. + /// + public Action OnError { get; set; } + } + + /// + /// Executes BuildAll with standard UI callbacks. + /// + public static void ExecuteBuildAll(BuildManager buildManager, BuildCallbacks callbacks) + { + if (buildManager.IsBuilding) return; + + callbacks.SetButtonsEnabled?.Invoke(false); + callbacks.ClearLog?.Invoke(); + + buildManager.StartBuildAll( + onComplete: () => + { + callbacks.SetButtonsEnabled?.Invoke(true); + callbacks.UpdateStatus?.Invoke("Build completed"); + callbacks.OnSuccess?.Invoke(); + EditorUtility.DisplayDialog("Build Successful", "Build completed successfully!", "OK"); + }, + onError: e => + { + callbacks.SetButtonsEnabled?.Invoke(true); + callbacks.UpdateStatus?.Invoke("Build failed"); + callbacks.OnError?.Invoke(); + EditorUtility.DisplayDialog("Build Failed", $"Build failed with error:\n{e.Message}", "OK"); + } + ); + } + + /// + /// Executes BuildCodeOnly with standard UI callbacks. + /// + public static void ExecuteBuildCodeOnly(BuildManager buildManager, BuildCallbacks callbacks) + { + if (buildManager.IsBuilding) return; + + callbacks.SetButtonsEnabled?.Invoke(false); + callbacks.ClearLog?.Invoke(); + + buildManager.StartBuildCodeOnly( + onComplete: () => + { + callbacks.SetButtonsEnabled?.Invoke(true); + callbacks.UpdateStatus?.Invoke("Code build completed"); + callbacks.OnSuccess?.Invoke(); + EditorUtility.DisplayDialog("Code Build Successful", "Code build completed successfully!", "OK"); + }, + onError: e => + { + callbacks.SetButtonsEnabled?.Invoke(true); + callbacks.UpdateStatus?.Invoke("Code build failed"); + callbacks.OnError?.Invoke(); + EditorUtility.DisplayDialog("Code Build Failed", $"Code build failed with error:\n{e.Message}", "OK"); + } + ); + } + + /// + /// Executes BuildAssetsOnly with standard UI callbacks. + /// + public static void ExecuteBuildAssetsOnly(BuildManager buildManager, BuildCallbacks callbacks) + { + if (buildManager.IsBuilding) return; + + callbacks.SetButtonsEnabled?.Invoke(false); + callbacks.ClearLog?.Invoke(); + + buildManager.StartBuildAssetsOnly( + onComplete: () => + { + callbacks.SetButtonsEnabled?.Invoke(true); + callbacks.UpdateStatus?.Invoke("Assets build completed"); + callbacks.OnSuccess?.Invoke(); + EditorUtility.DisplayDialog("Assets Build Successful", "Assets build completed successfully!", "OK"); + }, + onError: e => + { + callbacks.SetButtonsEnabled?.Invoke(true); + callbacks.UpdateStatus?.Invoke("Assets build failed"); + callbacks.OnError?.Invoke(); + EditorUtility.DisplayDialog("Assets Build Failed", $"Assets build failed with error:\n{e.Message}", "OK"); + } + ); + } + } +} diff --git a/UnityProject/Packages/com.jasonxudeveloper.jengine.core/Editor/BuildHelper.cs.meta b/UnityProject/Packages/com.jasonxudeveloper.jengine.core/Editor/BuildHelper.cs.meta new file mode 100644 index 00000000..e611aace --- /dev/null +++ b/UnityProject/Packages/com.jasonxudeveloper.jengine.core/Editor/BuildHelper.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d6c1ae76460924748872b68b94a3ce3f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/UnityProject/Packages/com.jasonxudeveloper.jengine.core/Editor/CustomEditor/BootstrapEditor.cs b/UnityProject/Packages/com.jasonxudeveloper.jengine.core/Editor/CustomEditor/BootstrapEditor.cs index bd4bfa1a..6e5259d0 100644 --- a/UnityProject/Packages/com.jasonxudeveloper.jengine.core/Editor/CustomEditor/BootstrapEditor.cs +++ b/UnityProject/Packages/com.jasonxudeveloper.jengine.core/Editor/CustomEditor/BootstrapEditor.cs @@ -15,12 +15,35 @@ namespace JEngine.Core.Editor.CustomEditor [UnityEditor.CustomEditor(typeof(Bootstrap))] public class BootstrapEditor : UnityEditor.Editor { + /// + /// Handler for creating inspector UI. If set, this is used instead of default UI. + /// Set by UI package via [InitializeOnLoad] to provide enhanced UI. + /// + /// + /// Parameters: SerializedObject, Bootstrap instance. + /// Returns: VisualElement to use as inspector content. + /// + public static Func CreateInspectorHandler; + private Bootstrap _bootstrap; private VisualElement _root; public override VisualElement CreateInspectorGUI() { _bootstrap = (Bootstrap)target; + + // If UI package provides enhanced editor, use it + if (CreateInspectorHandler != null) + { + return CreateInspectorHandler(serializedObject, _bootstrap); + } + + // Otherwise use default implementation + return CreateDefaultInspectorGUI(); + } + + private VisualElement CreateDefaultInspectorGUI() + { _root = new VisualElement(); // Add USS styling @@ -61,7 +84,7 @@ private void CreateServerSettingsGroup() var serverGroup = CreateGroup("Server Settings"); // Default Host Server - var defaultHostRow = CreateFormRow("Default Host Server"); + var defaultHostRow = CreateFormRow("Host Server"); var defaultHostField = new TextField(); defaultHostField.BindProperty(serializedObject.FindProperty(nameof(_bootstrap.defaultHostServer))); defaultHostField.AddToClassList("form-control"); @@ -92,7 +115,7 @@ private void CreateServerSettingsGroup() // Custom Fallback Server (conditionally visible) var fallbackContainer = new VisualElement(); fallbackContainer.name = "fallback-container"; - var fallbackRow = CreateFormRow("Custom Fallback Server"); + var fallbackRow = CreateFormRow("Fallback Server"); var fallbackField = new TextField(); fallbackField.BindProperty(serializedObject.FindProperty(nameof(_bootstrap.fallbackHostServer))); fallbackField.AddToClassList("form-control"); @@ -129,7 +152,7 @@ private void CreateAssetSettingsGroup() var assetGroup = CreateGroup("Asset Settings"); // Target Platform - var targetPlatformRow = CreateFormRow("Target Platform"); + var targetPlatformRow = CreateFormRow("Platform"); var targetPlatformField = new EnumField(_bootstrap.targetPlatform); targetPlatformField.AddToClassList("form-control"); EditorUIUtils.MakeTextResponsive(targetPlatformField); @@ -143,11 +166,11 @@ private void CreateAssetSettingsGroup() assetGroup.Add(targetPlatformRow); // Package Name Dropdown - var packageNameRow = CreateFormRow("Package Name"); + var packageNameRow = CreateFormRow("Package"); var packageChoices = EditorUtils.GetAvailableYooAssetPackages(); var packageNameField = new PopupField() { - choices = packageChoices.Any() ? packageChoices : new List { _bootstrap.packageName }, + choices = packageChoices.Count > 0 ? packageChoices : new List { _bootstrap.packageName }, value = _bootstrap.packageName }; packageNameField.AddToClassList("form-control"); @@ -161,11 +184,11 @@ private void CreateAssetSettingsGroup() assetGroup.Add(packageNameRow); // Hot Code Assembly Dropdown - var hotCodeRow = CreateFormRow("Hot Code Assembly"); - var hotCodeChoices = GetAvailableAsmdefFiles(); + var hotCodeRow = CreateFormRow("Code Assembly"); + var hotCodeChoices = EditorUtils.GetAvailableAsmdefFiles(); var hotCodeField = new PopupField() { - choices = hotCodeChoices.Any() ? hotCodeChoices : new List { _bootstrap.hotCodeName }, + choices = hotCodeChoices.Count > 0 ? hotCodeChoices : new List { _bootstrap.hotCodeName }, value = _bootstrap.hotCodeName }; hotCodeField.AddToClassList("form-control"); @@ -179,11 +202,11 @@ private void CreateAssetSettingsGroup() assetGroup.Add(hotCodeRow); // Hot Scene Dropdown - var hotSceneRow = CreateFormRow("Hot Scene"); - var hotSceneChoices = GetAvailableHotScenes(); + var hotSceneRow = CreateFormRow("Scene"); + var hotSceneChoices = EditorUtils.GetAvailableHotScenes(); var hotSceneField = new PopupField() { - choices = hotSceneChoices.Any() ? hotSceneChoices : new List { _bootstrap.selectedHotScene }, + choices = hotSceneChoices.Count > 0 ? hotSceneChoices : new List { _bootstrap.selectedHotScene }, value = _bootstrap.selectedHotScene }; hotSceneField.AddToClassList("form-control"); @@ -197,11 +220,11 @@ private void CreateAssetSettingsGroup() assetGroup.Add(hotSceneRow); // Hot Update Entry Class Dropdown - var hotClassRow = CreateFormRow("Hot Update Entry Class"); - var hotClassChoices = GetAvailableHotClasses(); + var hotClassRow = CreateFormRow("Entry Class"); + var hotClassChoices = EditorUtils.GetAvailableHotClasses(_bootstrap.hotCodeName); var hotClassField = new PopupField() { - choices = hotClassChoices.Any() ? hotClassChoices : new List { _bootstrap.hotUpdateClassName }, + choices = hotClassChoices.Count > 0 ? hotClassChoices : new List { _bootstrap.hotUpdateClassName }, value = _bootstrap.hotUpdateClassName }; hotClassField.AddToClassList("form-control"); @@ -215,11 +238,11 @@ private void CreateAssetSettingsGroup() assetGroup.Add(hotClassRow); // Hot Update Entry Method Dropdown - var hotMethodRow = CreateFormRow("Hot Update Entry Method"); - var hotMethodChoices = GetAvailableHotMethods(); + var hotMethodRow = CreateFormRow("Entry Method"); + var hotMethodChoices = EditorUtils.GetAvailableHotMethods(_bootstrap.hotCodeName, _bootstrap.hotUpdateClassName); var hotMethodField = new PopupField() { - choices = hotMethodChoices.Any() + choices = hotMethodChoices.Count > 0 ? hotMethodChoices : new List { _bootstrap.hotUpdateMethodName }, value = _bootstrap.hotUpdateMethodName @@ -235,11 +258,11 @@ private void CreateAssetSettingsGroup() assetGroup.Add(hotMethodRow); // AOT DLL List File Dropdown - var aotRow = CreateFormRow("AOT DLL List File"); - var aotChoices = GetAvailableAOTDataFiles(); + var aotRow = CreateFormRow("AOT DLL List"); + var aotChoices = EditorUtils.GetAvailableAOTDataFiles(); var aotField = new PopupField() { - choices = aotChoices.Any() ? aotChoices : new List { _bootstrap.aotDllListFilePath }, + choices = aotChoices.Count > 0 ? aotChoices : new List { _bootstrap.aotDllListFilePath }, value = _bootstrap.aotDllListFilePath }; aotField.AddToClassList("form-control"); @@ -260,11 +283,11 @@ private void CreateSecuritySettingsGroup() var securityGroup = CreateGroup("Security Settings"); // Dynamic Secret Key Dropdown - var dynamicKeyRow = CreateFormRow("Dynamic Secret Key"); - var dynamicKeyChoices = GetAvailableDynamicSecretKeys(); + var dynamicKeyRow = CreateFormRow("Secret Key"); + var dynamicKeyChoices = EditorUtils.GetAvailableDynamicSecretKeys(); var dynamicKeyField = new PopupField() { - choices = dynamicKeyChoices.Any() + choices = dynamicKeyChoices.Count > 0 ? dynamicKeyChoices : new List { _bootstrap.dynamicSecretKeyPath }, value = _bootstrap.dynamicSecretKeyPath @@ -284,7 +307,7 @@ private void CreateSecuritySettingsGroup() var manifestConfigFile = bundleConfig.ManifestConfigScriptableObject; var bundleConfigFile = bundleConfig.BundleConfigScriptableObject; - var encryptionRow = CreateFormRow("Encryption Option"); + var encryptionRow = CreateFormRow("Encryption"); var encryptionField = new EnumField(_bootstrap.encryptionOption); // Manifest Config Object Field @@ -376,7 +399,7 @@ private void CreateUISettingsGroup() uiGroup.Add(statusRow); // Download Progress Text - var progressTextRow = CreateFormRow("Download Progress Text"); + var progressTextRow = CreateFormRow("Progress Text"); var progressTextField = new ObjectField() { objectType = typeof(TMPro.TextMeshProUGUI), @@ -389,7 +412,7 @@ private void CreateUISettingsGroup() uiGroup.Add(progressTextRow); // Download Progress Bar - var progressBarRow = CreateFormRow("Download Progress Bar"); + var progressBarRow = CreateFormRow("Progress Bar"); var progressBarField = new ObjectField() { objectType = typeof(UnityEngine.UI.Slider), @@ -510,7 +533,6 @@ private VisualElement CreateGroup(string title) return group; } - private StyleSheet CreateStyleSheet() { return StyleSheetLoader.LoadPackageStyleSheet(); @@ -528,126 +550,5 @@ private VisualElement CreateFormRow(string labelText) return row; } - - // Helper methods to get available options - - private List GetAvailableAsmdefFiles() - { - var asmdefGuids = AssetDatabase.FindAssets("t:AssemblyDefinitionAsset", new[] { "Assets/HotUpdate" }); - return asmdefGuids - .Select(AssetDatabase.GUIDToAssetPath) - .Select(System.IO.Path.GetFileNameWithoutExtension) - .Select(asmdefName => asmdefName + ".dll") - .ToList(); - } - - private List GetAvailableHotScenes() - { - var sceneGuids = AssetDatabase.FindAssets("t:Scene", new[] { "Assets/HotUpdate" }); - return sceneGuids - .Select(AssetDatabase.GUIDToAssetPath) - .ToList(); - } - - private List GetAvailableHotClasses() - { - try - { - var assemblyName = System.IO.Path.GetFileNameWithoutExtension(_bootstrap.hotCodeName); - var assembly = AppDomain.CurrentDomain.GetAssemblies() - .FirstOrDefault(a => a.GetName().Name == assemblyName); - - if (assembly != null) - { - return assembly.GetTypes() - .Where(t => t.IsClass && t.IsPublic) - .Where(t => t.GetMethods(BindingFlags.Public | BindingFlags.Static).Any()) - .Select(t => t.FullName) - .OrderBy(n => n) - .ToList(); - } - } - catch (Exception ex) - { - Debug.LogWarning($"Failed to get YooAsset packages: {ex.Message}"); - } - - return new List(); - } - - private List GetAvailableHotMethods() - { - try - { - if (!string.IsNullOrEmpty(_bootstrap.hotUpdateClassName)) - { - var type = Type.GetType(_bootstrap.hotUpdateClassName); - if (type == null) - { - var assemblyName = System.IO.Path.GetFileNameWithoutExtension(_bootstrap.hotCodeName); - var assembly = AppDomain.CurrentDomain.GetAssemblies() - .FirstOrDefault(a => a.GetName().Name == assemblyName); - if (assembly != null) - { - type = assembly.GetType(_bootstrap.hotUpdateClassName); - } - } - - if (type != null) - { - return type.GetMethods(BindingFlags.Public | BindingFlags.Static) - .Where(m => m.ReturnType == typeof(void) || - m.ReturnType == typeof(UniTask) || - m.ReturnType == typeof(System.Threading.Tasks.Task)) - .Where(m => m.GetParameters().Length == 0) - .Select(m => m.Name) - .OrderBy(n => n) - .ToList(); - } - } - } - catch (Exception ex) - { - Debug.LogWarning($"Failed to get YooAsset packages: {ex.Message}"); - } - - return new List(); - } - - private List GetAvailableDynamicSecretKeys() - { - var secretKeys = new List(); - - // Search for .bytes files that might be secret keys - var bytesGuids = AssetDatabase.FindAssets("t:TextAsset", new[] { "Assets" }); - var secretKeyFiles = bytesGuids - .Select(AssetDatabase.GUIDToAssetPath) - .Where(path => path.EndsWith(".bytes") && - (path.Contains("Secret") || path.Contains("Obfuz") || path.Contains("Key"))) - .ToList(); - - if (secretKeyFiles.Any()) - { - secretKeys.AddRange(secretKeyFiles); - } - - // Add default if no keys found - if (secretKeys.Count == 0) - { - secretKeys.Add("Assets/HotUpdate/Obfuz/DynamicSecretKey.bytes"); - } - - return secretKeys; - } - - private List GetAvailableAOTDataFiles() - { - var aotDataGuids = AssetDatabase.FindAssets("t:TextAsset", new[] { "Assets/HotUpdate/Compiled" }); - return aotDataGuids - .Select(AssetDatabase.GUIDToAssetPath) - .Where(path => path.EndsWith(".bytes")) - .OrderBy(path => path) - .ToList(); - } } } \ No newline at end of file diff --git a/UnityProject/Packages/com.jasonxudeveloper.jengine.core/Editor/CustomEditor/Panel.cs b/UnityProject/Packages/com.jasonxudeveloper.jengine.core/Editor/CustomEditor/Panel.cs index 2d99de50..0170619d 100644 --- a/UnityProject/Packages/com.jasonxudeveloper.jengine.core/Editor/CustomEditor/Panel.cs +++ b/UnityProject/Packages/com.jasonxudeveloper.jengine.core/Editor/CustomEditor/Panel.cs @@ -23,6 +23,7 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. +using System; using UnityEditor; using UnityEditor.SceneManagement; using UnityEditor.UIElements; @@ -34,6 +35,16 @@ namespace JEngine.Core.Editor.CustomEditor { public class Panel : EditorWindow { + /// + /// Handler for creating panel content. If set, this is used instead of default UI. + /// Set by UI package via [InitializeOnLoad] to provide enhanced UI. + /// + /// + /// Parameters: Panel instance, BuildManager, Settings. + /// Returns: VisualElement to use as panel content. + /// + public static Func CreatePanelContentHandler; + private Settings _settings; private VisualElement _root; private Button _buildAllButton; @@ -51,6 +62,19 @@ private void CreateGUI() // Initialize build manager _buildManager = new BuildManager(_settings, LogMessage); + // If UI package provides enhanced editor, use it + if (CreatePanelContentHandler != null) + { + _root.Add(CreatePanelContentHandler(this, _buildManager, _settings)); + return; + } + + // Otherwise use default implementation + CreateDefaultGUI(); + } + + private void CreateDefaultGUI() + { // Load stylesheets - Panel first, then Common to override var panelStyleSheet = StyleSheetLoader.LoadPackageStyleSheet(); if (panelStyleSheet != null) @@ -352,71 +376,32 @@ private void CreateStatusSection(VisualElement parent) private void BuildAll() { - if (_buildManager.IsBuilding) return; - - SetBuildButtonsEnabled(false); - ClearLog(); - - _buildManager.StartBuildAll( - onComplete: () => - { - SetBuildButtonsEnabled(true); - _statusLabel.text = "Build completed"; - EditorUtility.DisplayDialog("Build Successful", "Build completed successfully!", "OK"); - }, - onError: e => - { - SetBuildButtonsEnabled(true); - _statusLabel.text = "Build failed"; - EditorUtility.DisplayDialog("Build Failed", $"Build failed with error:\n{e.Message}", "OK"); - } - ); + BuildHelper.ExecuteBuildAll(_buildManager, new BuildHelper.BuildCallbacks + { + SetButtonsEnabled = SetBuildButtonsEnabled, + ClearLog = ClearLog, + UpdateStatus = msg => _statusLabel.text = msg + }); } private void BuildCodeOnly() { - if (_buildManager.IsBuilding) return; - - SetBuildButtonsEnabled(false); - ClearLog(); - - _buildManager.StartBuildCodeOnly( - onComplete: () => - { - SetBuildButtonsEnabled(true); - _statusLabel.text = "Code build completed"; - EditorUtility.DisplayDialog("Code Build Successful", "Code build completed successfully!", "OK"); - }, - onError: e => - { - SetBuildButtonsEnabled(true); - _statusLabel.text = "Code build failed"; - EditorUtility.DisplayDialog("Code Build Failed", $"Code build failed with error:\n{e.Message}", "OK"); - } - ); + BuildHelper.ExecuteBuildCodeOnly(_buildManager, new BuildHelper.BuildCallbacks + { + SetButtonsEnabled = SetBuildButtonsEnabled, + ClearLog = ClearLog, + UpdateStatus = msg => _statusLabel.text = msg + }); } private void BuildAssetsOnly() { - if (_buildManager.IsBuilding) return; - - SetBuildButtonsEnabled(false); - ClearLog(); - - _buildManager.StartBuildAssetsOnly( - onComplete: () => - { - SetBuildButtonsEnabled(true); - _statusLabel.text = "Assets build completed"; - EditorUtility.DisplayDialog("Assets Build Successful", "Assets build completed successfully!", "OK"); - }, - onError: e => - { - SetBuildButtonsEnabled(true); - _statusLabel.text = "Assets build failed"; - EditorUtility.DisplayDialog("Assets Build Failed", $"Assets build failed with error:\n{e.Message}", "OK"); - } - ); + BuildHelper.ExecuteBuildAssetsOnly(_buildManager, new BuildHelper.BuildCallbacks + { + SetButtonsEnabled = SetBuildButtonsEnabled, + ClearLog = ClearLog, + UpdateStatus = msg => _statusLabel.text = msg + }); } private void SetBuildButtonsEnabled(bool enabled) @@ -427,17 +412,25 @@ private void SetBuildButtonsEnabled(bool enabled) } /// - /// State machine update called every editor frame during build. + /// Logs a message to the panel's log view and updates status. /// - private void LogMessage(string message, bool isError = false) + /// The message to log. + /// Whether this is an error message. + public void LogMessage(string message, bool isError = false) { - var logEntry = new Label(message); - logEntry.AddToClassList(isError ? "log-error" : "log-info"); - - _logScrollView.Add(logEntry); - _logScrollView.ScrollTo(logEntry); + // Only update UI elements if they exist (they won't exist when using enhanced PanelUI) + if (_logScrollView != null) + { + var logEntry = new Label(message); + logEntry.AddToClassList(isError ? "log-error" : "log-info"); + _logScrollView.Add(logEntry); + _logScrollView.ScrollTo(logEntry); + } - _statusLabel.text = message; + if (_statusLabel != null) + { + _statusLabel.text = message; + } if (isError) Debug.LogError(message); @@ -447,8 +440,15 @@ private void LogMessage(string message, bool isError = false) private void ClearLog() { - _logScrollView.Clear(); - _statusLabel.text = "Ready to build"; + if (_logScrollView != null) + { + _logScrollView.Clear(); + } + + if (_statusLabel != null) + { + _statusLabel.text = "Ready to build"; + } } } } \ No newline at end of file diff --git a/UnityProject/Packages/com.jasonxudeveloper.jengine.core/Editor/CustomEditor/SettingsUIBuilder.cs b/UnityProject/Packages/com.jasonxudeveloper.jengine.core/Editor/CustomEditor/SettingsUIBuilder.cs index 717f5f25..c6424506 100644 --- a/UnityProject/Packages/com.jasonxudeveloper.jengine.core/Editor/CustomEditor/SettingsUIBuilder.cs +++ b/UnityProject/Packages/com.jasonxudeveloper.jengine.core/Editor/CustomEditor/SettingsUIBuilder.cs @@ -44,13 +44,13 @@ public static VisualElement CreatePackageSettingsGroup(Settings settings) var packageGroup = EditorUIUtils.CreateGroup("Package Settings"); // Package Name field (dropdown or text field based on useDropdown) - var packageNameRow = EditorUIUtils.CreateFormRow("Package Name"); + var packageNameRow = EditorUIUtils.CreateFormRow("Package"); // Use PopupField for Panel (with available packages) var packageChoices = EditorUtils.GetAvailableYooAssetPackages(); var packageNameField = new PopupField() { - choices = packageChoices.Any() ? packageChoices : new List { settings.packageName }, + choices = packageChoices.Count > 0 ? packageChoices : new List { settings.packageName }, value = settings.packageName }; packageNameField.AddToClassList("form-control"); @@ -88,7 +88,7 @@ public static VisualElement CreatePackageSettingsGroup(Settings settings) settings.Save(); }) { - text = "Set to Current Active Target" + text = "Use Active" }; setActiveButton.AddToClassList("form-control"); EditorUIUtils.MakeActionButtonResponsive(setActiveButton); @@ -118,7 +118,7 @@ public static VisualElement CreateBuildOptionsGroup(Settings settings) buildGroup.Add(clearCacheRow); // Use Asset Dependency DB Toggle - var useAssetDBRow = EditorUIUtils.CreateFormRow("Use Asset Dependency DB"); + var useAssetDBRow = EditorUIUtils.CreateFormRow("Use Asset Depend DB"); var useAssetDBToggle = new Toggle() { value = settings.useAssetDependDB @@ -137,7 +137,7 @@ public static VisualElement CreateBuildOptionsGroup(Settings settings) var manifestConfigFile = bundleConfig.ManifestConfigScriptableObject; var bundleConfigFile = bundleConfig.BundleConfigScriptableObject; - var encryptionRow = EditorUIUtils.CreateFormRow("Encryption Option"); + var encryptionRow = EditorUIUtils.CreateFormRow("Encryption"); var encryptionField = new EnumField(settings.encryptionOption); // Manifest Config Object Field diff --git a/UnityProject/Packages/com.jasonxudeveloper.jengine.core/Editor/EditorUtils.cs b/UnityProject/Packages/com.jasonxudeveloper.jengine.core/Editor/EditorUtils.cs index 3ec2e69b..3346425a 100644 --- a/UnityProject/Packages/com.jasonxudeveloper.jengine.core/Editor/EditorUtils.cs +++ b/UnityProject/Packages/com.jasonxudeveloper.jengine.core/Editor/EditorUtils.cs @@ -25,6 +25,10 @@ using System; using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Cysharp.Threading.Tasks; +using UnityEditor; using UnityEngine; using YooAsset.Editor; @@ -64,5 +68,187 @@ public static List GetAvailableYooAssetPackages() return packages; } + + /// + /// Gets the YooAsset package name that contains the specified asset path. + /// + /// The asset path to search for + /// Fallback package name if not found (default: "main") + /// The package name containing the asset, or fallback if not found + public static string GetPackageNameForAsset(string assetPath, string fallbackPackageName = "main") + { + try + { + if (AssetBundleCollectorSettingData.Setting != null && + AssetBundleCollectorSettingData.Setting.Packages != null) + { + foreach (var package in AssetBundleCollectorSettingData.Setting.Packages) + { + if (package.Groups == null) continue; + + foreach (var group in package.Groups) + { + if (group == null || group.Collectors == null) continue; + + foreach (var collector in group.Collectors) + { + if (collector == null || string.IsNullOrEmpty(collector.CollectPath)) continue; + + // Check if asset path matches collector path + var collectPath = collector.CollectPath; + if (assetPath.StartsWith(collectPath, StringComparison.OrdinalIgnoreCase)) + { + return package.PackageName; + } + } + } + } + } + } + catch (Exception ex) + { + Debug.LogWarning($"Failed to get package for asset {assetPath}: {ex.Message}"); + } + + return fallbackPackageName; + } + + /// + /// Gets available .asmdef files from Assets/HotUpdate directory. + /// + public static List GetAvailableAsmdefFiles() + { + var asmdefGuids = AssetDatabase.FindAssets("t:AssemblyDefinitionAsset", new[] { "Assets/HotUpdate" }); + return asmdefGuids + .Select(AssetDatabase.GUIDToAssetPath) + .Select(System.IO.Path.GetFileNameWithoutExtension) + .Select(asmdefName => asmdefName + ".dll") + .ToList(); + } + + /// + /// Gets available scene files from Assets/HotUpdate directory. + /// + public static List GetAvailableHotScenes() + { + var sceneGuids = AssetDatabase.FindAssets("t:Scene", new[] { "Assets/HotUpdate" }); + return sceneGuids + .Select(AssetDatabase.GUIDToAssetPath) + .ToList(); + } + + /// + /// Gets available public classes from the specified hot code assembly. + /// + /// The hot code assembly name (e.g., "HotUpdate.Code.dll"). + public static List GetAvailableHotClasses(string hotCodeName) + { + try + { + var assemblyName = System.IO.Path.GetFileNameWithoutExtension(hotCodeName); + var assembly = AppDomain.CurrentDomain.GetAssemblies() + .FirstOrDefault(a => a.GetName().Name == assemblyName); + + if (assembly != null) + { + return assembly.GetTypes() + .Where(t => t.IsClass && t.IsPublic) + .Where(t => t.GetMethods(BindingFlags.Public | BindingFlags.Static).Length > 0) + .Select(t => t.FullName) + .OrderBy(n => n) + .ToList(); + } + } + catch (Exception ex) + { + Debug.LogWarning($"Failed to get hot classes: {ex.Message}"); + } + + return new List(); + } + + /// + /// Gets available static methods from the specified hot update entry class. + /// + /// The hot code assembly name. + /// The full class name. + public static List GetAvailableHotMethods(string hotCodeName, string hotUpdateClassName) + { + try + { + if (!string.IsNullOrEmpty(hotUpdateClassName)) + { + var type = Type.GetType(hotUpdateClassName); + if (type == null) + { + var assemblyName = System.IO.Path.GetFileNameWithoutExtension(hotCodeName); + var assembly = AppDomain.CurrentDomain.GetAssemblies() + .FirstOrDefault(a => a.GetName().Name == assemblyName); + if (assembly != null) + { + type = assembly.GetType(hotUpdateClassName); + } + } + + if (type != null) + { + return type.GetMethods(BindingFlags.Public | BindingFlags.Static) + .Where(m => m.ReturnType == typeof(void) || + m.ReturnType == typeof(UniTask) || + m.ReturnType == typeof(System.Threading.Tasks.Task)) + .Where(m => m.GetParameters().Length == 0) + .Select(m => m.Name) + .OrderBy(n => n) + .ToList(); + } + } + } + catch (Exception ex) + { + Debug.LogWarning($"Failed to get hot methods: {ex.Message}"); + } + + return new List(); + } + + /// + /// Gets available dynamic secret key files (.bytes) from the project. + /// + public static List GetAvailableDynamicSecretKeys() + { + var secretKeys = new List(); + + var bytesGuids = AssetDatabase.FindAssets("t:TextAsset", new[] { "Assets" }); + var secretKeyFiles = bytesGuids + .Select(AssetDatabase.GUIDToAssetPath) + .Where(path => path.EndsWith(".bytes") && + (path.Contains("Secret") || path.Contains("Obfuz") || path.Contains("Key"))) + .ToList(); + + if (secretKeyFiles.Count > 0) + { + secretKeys.AddRange(secretKeyFiles); + } + + if (secretKeys.Count == 0) + { + secretKeys.Add("Assets/HotUpdate/Obfuz/DynamicSecretKey.bytes"); + } + + return secretKeys; + } + + /// + /// Gets available AOT DLL list files from Assets/HotUpdate/Compiled directory. + /// + public static List GetAvailableAOTDataFiles() + { + var aotDataGuids = AssetDatabase.FindAssets("t:TextAsset", new[] { "Assets/HotUpdate/Compiled" }); + return aotDataGuids + .Select(AssetDatabase.GUIDToAssetPath) + .Where(path => path.EndsWith(".bytes")) + .OrderBy(path => path) + .ToList(); + } } } \ No newline at end of file diff --git a/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor.meta b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor.meta new file mode 100644 index 00000000..7f463c3f --- /dev/null +++ b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: ba1b9808010af4003b96181aa98f3918 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Components.meta b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Components.meta new file mode 100644 index 00000000..de9c8199 --- /dev/null +++ b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Components.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: f13134207fb8a46f9b3309dff542e8a8 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Components/Base.meta b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Components/Base.meta new file mode 100644 index 00000000..1757abda --- /dev/null +++ b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Components/Base.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 1db8ebb188c51439294d1484e3151d19 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Components/Base/JComponent.cs b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Components/Base/JComponent.cs new file mode 100644 index 00000000..f6d6fd51 --- /dev/null +++ b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Components/Base/JComponent.cs @@ -0,0 +1,148 @@ +// JComponent.cs +// +// Author: +// JasonXuDeveloper +// +// Copyright (c) 2025 JEngine +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +using UnityEngine.UIElements; + +namespace JEngine.UI.Editor.Components +{ + /// + /// Base class for all JEngine Editor UI components. + /// Provides fluent API for common operations. + /// + public abstract class JComponent : VisualElement + { + /// + /// Creates a new JComponent with the specified base class. + /// + /// The primary CSS class for this component. + protected JComponent(string baseClassName) + { + if (!string.IsNullOrEmpty(baseClassName)) + { + AddToClassList(baseClassName); + } + } + + /// + /// Adds a CSS class to this component. + /// + /// The class name to add. + /// This component for chaining. + public JComponent WithClass(string className) + { + AddToClassList(className); + return this; + } + + /// + /// Sets the name of this component. + /// + /// The element name. + /// This component for chaining. + public JComponent WithName(string elementName) + { + name = elementName; + return this; + } + + /// + /// Adds child elements to this component. + /// + /// The elements to add. + /// This component for chaining. + public JComponent Add(params VisualElement[] children) + { + foreach (var child in children) + { + if (child != null) + { + base.Add(child); + } + } + return this; + } + + /// + /// Sets the flex grow value. + /// + /// The flex grow value. + /// This component for chaining. + public JComponent WithFlexGrow(float value) + { + style.flexGrow = value; + return this; + } + + /// + /// Sets the flex shrink value. + /// + /// The flex shrink value. + /// This component for chaining. + public JComponent WithFlexShrink(float value) + { + style.flexShrink = value; + return this; + } + + /// + /// Sets margin on all sides. + /// + /// The margin value. + /// This component for chaining. + public JComponent WithMargin(float value) + { + style.marginTop = value; + style.marginRight = value; + style.marginBottom = value; + style.marginLeft = value; + return this; + } + + /// + /// Sets padding on all sides. + /// + /// The padding value. + /// This component for chaining. + public JComponent WithPadding(float value) + { + style.paddingTop = value; + style.paddingRight = value; + style.paddingBottom = value; + style.paddingLeft = value; + return this; + } + + /// + /// Sets the visibility of this component. + /// + /// Whether the component should be visible. + /// This component for chaining. + public JComponent WithVisibility(bool visible) + { + style.display = visible ? DisplayStyle.Flex : DisplayStyle.None; + return this; + } + } +} diff --git a/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Components/Base/JComponent.cs.meta b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Components/Base/JComponent.cs.meta new file mode 100644 index 00000000..0d71af0c --- /dev/null +++ b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Components/Base/JComponent.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8058a8a7925414684b4650cfbe3e8440 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Components/Button.meta b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Components/Button.meta new file mode 100644 index 00000000..4a796a11 --- /dev/null +++ b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Components/Button.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 766e8810bfa1c458191dc6a215d23403 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Components/Button/JButton.cs b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Components/Button/JButton.cs new file mode 100644 index 00000000..9adf5bd5 --- /dev/null +++ b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Components/Button/JButton.cs @@ -0,0 +1,310 @@ +// JButton.cs +// +// Author: +// JasonXuDeveloper +// +// Copyright (c) 2025 JEngine +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +using System; +using JEngine.UI.Editor.Theming; +using UnityEngine; +using UnityEngine.UIElements; + +namespace JEngine.UI.Editor.Components.Button +{ + /// + /// A themed button with variant styling. + /// + public class JButton : UnityEngine.UIElements.Button + { + private ButtonVariant _variant; + private Color _baseColor; + private Color _hoverColor; + private Color _activeColor; + + /// + /// Creates a new button with text and optional click action. + /// + /// The button text. + /// Optional click handler. + /// The button variant (Primary, Secondary, Success, Danger, Warning). + public JButton(string text, Action onClick = null, ButtonVariant variant = ButtonVariant.Primary) + { + this.text = text; + _variant = variant; + + AddToClassList("j-button"); + + // Apply base styles + ApplyBaseStyles(); + + // Apply variant + SetVariant(variant); + + // Register click handler + if (onClick != null) + { + clicked += onClick; + } + + // Register hover/active events for color changes + RegisterCallback(OnMouseEnter); + RegisterCallback(OnMouseLeave); + RegisterCallback(OnMouseDown); + RegisterCallback(OnMouseUp); + RegisterCallback(OnFocusIn); + RegisterCallback(OnFocusOut); + RegisterCallback(OnClick); + } + + /// + /// Gets or sets the button variant. + /// + public ButtonVariant Variant + { + get => _variant; + set => SetVariant(value); + } + + private void ApplyBaseStyles() + { + // Glassmorphic border radius (8px) + style.borderTopLeftRadius = Tokens.BorderRadius.MD; + style.borderTopRightRadius = Tokens.BorderRadius.MD; + style.borderBottomLeftRadius = Tokens.BorderRadius.MD; + style.borderBottomRightRadius = Tokens.BorderRadius.MD; + + // Enhanced padding (6px vertical, 14px horizontal) + style.paddingTop = Tokens.Spacing.Sm; + style.paddingRight = Tokens.Spacing.Lg; + style.paddingBottom = Tokens.Spacing.Sm; + style.paddingLeft = Tokens.Spacing.Lg; + + // Remove all margins + style.marginLeft = 0; + style.marginRight = 0; + style.marginTop = 0; + style.marginBottom = 0; + + // Enhanced min-height (28px instead of 22px) + style.minHeight = 28; + + // Font styling + style.fontSize = Tokens.FontSize.Base; + style.unityFontStyleAndWeight = FontStyle.Bold; + + // No border for glassmorphic look + style.borderTopWidth = 0; + style.borderRightWidth = 0; + style.borderBottomWidth = 0; + style.borderLeftWidth = 0; + + // Don't set flexGrow/flexShrink here - let parent control layout + style.overflow = Overflow.Hidden; + style.textOverflow = TextOverflow.Ellipsis; + style.whiteSpace = WhiteSpace.NoWrap; + + // Smooth glassmorphic transitions + JTheme.ApplyTransition(this); + } + + /// + /// Sets the button variant and updates colors. + /// + /// The variant to apply. + /// This button for chaining. + public JButton SetVariant(ButtonVariant variant) + { + // Remove existing variant classes + RemoveFromClassList("j-button--primary"); + RemoveFromClassList("j-button--secondary"); + RemoveFromClassList("j-button--success"); + RemoveFromClassList("j-button--danger"); + RemoveFromClassList("j-button--warning"); + + _variant = variant; + _baseColor = JTheme.GetButtonColor(variant); + _hoverColor = JTheme.GetButtonHoverColor(variant); + _activeColor = JTheme.GetButtonActiveColor(variant); + + // Add new variant class + var variantClass = variant switch + { + ButtonVariant.Primary => "j-button--primary", + ButtonVariant.Secondary => "j-button--secondary", + ButtonVariant.Success => "j-button--success", + ButtonVariant.Danger => "j-button--danger", + ButtonVariant.Warning => "j-button--warning", + _ => "j-button--primary" + }; + AddToClassList(variantClass); + + // Apply base color + style.backgroundColor = _baseColor; + + // Set text color based on variant using theme tokens + style.color = variant switch + { + ButtonVariant.Primary => Tokens.Colors.PrimaryText, + ButtonVariant.Secondary => Tokens.Colors.SecondaryText, + ButtonVariant.Success => Tokens.Colors.PrimaryText, // Same as primary + ButtonVariant.Danger => Tokens.Colors.PrimaryText, // Same as primary + ButtonVariant.Warning => Tokens.Colors.PrimaryText, // Same as primary + _ => Tokens.Colors.PrimaryText + }; + + return this; + } + + private void OnMouseEnter(MouseEnterEvent evt) + { + if (!enabledSelf) return; + style.backgroundColor = _hoverColor; + } + + private void OnMouseLeave(MouseLeaveEvent evt) + { + style.backgroundColor = _baseColor; + } + + private void OnMouseDown(MouseDownEvent evt) + { + if (!enabledSelf) return; + style.backgroundColor = _activeColor; + } + + private void OnMouseUp(MouseUpEvent evt) + { + if (!enabledSelf) return; + style.backgroundColor = _hoverColor; + } + + private void OnFocusIn(FocusInEvent evt) + { + // Add subtle focus ring for keyboard navigation + style.borderTopWidth = 1; + style.borderRightWidth = 1; + style.borderBottomWidth = 1; + style.borderLeftWidth = 1; + + // Light mode: lighter grey border + // Dark mode: cyan border + var focusColor = Tokens.IsDarkTheme + ? Tokens.Colors.BorderFocus + : Tokens.Colors.BorderSubtle; + + style.borderTopColor = focusColor; + style.borderRightColor = focusColor; + style.borderBottomColor = focusColor; + style.borderLeftColor = focusColor; + } + + private void OnFocusOut(FocusOutEvent evt) + { + // Remove focus ring + style.borderTopWidth = 0; + style.borderRightWidth = 0; + style.borderBottomWidth = 0; + style.borderLeftWidth = 0; + } + + private void OnClick(ClickEvent evt) + { + // Blur the button after click to remove focus border + Blur(); + } + + /// + /// Sets the button text. + /// + /// The new text. + /// This button for chaining. + public JButton WithText(string buttonText) + { + text = buttonText; + return this; + } + + /// + /// Adds a CSS class. + /// + /// The class name. + /// This button for chaining. + public JButton WithClass(string className) + { + AddToClassList(className); + return this; + } + + /// + /// Sets whether the button is enabled. + /// + /// Whether the button should be enabled. + /// This button for chaining. + public JButton WithEnabled(bool isEnabled) + { + SetEnabled(isEnabled); + return this; + } + + /// + /// Sets the button to fill available width. + /// + /// This button for chaining. + public JButton FullWidth() + { + style.flexGrow = 1; + style.flexShrink = 1; + style.minWidth = 60; + style.maxHeight = 26; + // Remove horizontal padding for edge-to-edge fill + style.paddingLeft = 0; + style.paddingRight = 0; + return this; + } + + /// + /// Makes the button compact (smaller padding). + /// + /// This button for chaining. + public JButton Compact() + { + style.paddingTop = 2; + style.paddingBottom = 2; + style.paddingLeft = 6; + style.paddingRight = 6; + style.minHeight = 18; + style.maxHeight = 20; + style.fontSize = 10; + return this; + } + + /// + /// Sets the minimum width. + /// + /// This button for chaining. + public JButton WithMinWidth(float minWidth) + { + style.minWidth = minWidth; + return this; + } + } +} diff --git a/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Components/Button/JButton.cs.meta b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Components/Button/JButton.cs.meta new file mode 100644 index 00000000..3d588cf0 --- /dev/null +++ b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Components/Button/JButton.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 35a816b43be7d4b609472d613a88bc7d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Components/Button/JButtonGroup.cs b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Components/Button/JButtonGroup.cs new file mode 100644 index 00000000..ab3ee932 --- /dev/null +++ b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Components/Button/JButtonGroup.cs @@ -0,0 +1,118 @@ +// JButtonGroup.cs +// +// Author: +// JasonXuDeveloper +// +// Copyright (c) 2025 JEngine +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +using JEngine.UI.Editor.Theming; +using UnityEngine.UIElements; + +namespace JEngine.UI.Editor.Components.Button +{ + /// + /// A horizontal row of buttons with responsive wrapping. + /// + public class JButtonGroup : JComponent + { + /// + /// Creates a new button group. + /// + /// The buttons to include in the group. + public JButtonGroup(params VisualElement[] buttons) : base("j-button-group") + { + style.flexDirection = FlexDirection.Row; + style.flexWrap = Wrap.Wrap; + style.alignItems = Align.Center; + + foreach (var button in buttons) + { + if (button != null) + { + // Apply group styling to buttons - compact + button.style.marginRight = Tokens.Spacing.Sm; + button.style.marginBottom = Tokens.Spacing.Xs; + button.style.flexGrow = 1; + button.style.flexShrink = 0; + button.style.minWidth = 100; + base.Add(button); + } + } + + // Remove right margin from last child + if (childCount > 0) + { + this[childCount - 1].style.marginRight = 0; + } + } + + /// + /// Adds buttons to this group. + /// + public new JButtonGroup Add(params VisualElement[] buttons) + { + foreach (var button in buttons) + { + if (button != null) + { + button.style.marginRight = Tokens.Spacing.Sm; + button.style.marginBottom = Tokens.Spacing.Xs; + button.style.flexGrow = 1; + button.style.flexShrink = 0; + button.style.minWidth = 100; + base.Add(button); + } + } + + // Update margins + for (int i = 0; i < childCount; i++) + { + this[i].style.marginRight = i < childCount - 1 ? Tokens.Spacing.Sm : 0; + } + + return this; + } + + /// + /// Disables flex-wrap (buttons won't wrap to next line). + /// + /// This button group for chaining. + public JButtonGroup NoWrap() + { + style.flexWrap = Wrap.NoWrap; + return this; + } + + /// + /// Sets buttons to fixed width (no flex grow). + /// + /// This button group for chaining. + public JButtonGroup FixedWidth() + { + for (int i = 0; i < childCount; i++) + { + this[i].style.flexGrow = 0; + this[i].style.flexBasis = StyleKeyword.Auto; + } + return this; + } + } +} diff --git a/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Components/Button/JButtonGroup.cs.meta b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Components/Button/JButtonGroup.cs.meta new file mode 100644 index 00000000..747c1f87 --- /dev/null +++ b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Components/Button/JButtonGroup.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: cd91e7e86818347a1920df64032cc1d4 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Components/Button/JToggleButton.cs b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Components/Button/JToggleButton.cs new file mode 100644 index 00000000..79bbf96e --- /dev/null +++ b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Components/Button/JToggleButton.cs @@ -0,0 +1,253 @@ +// JToggleButton.cs +// +// Author: +// JasonXuDeveloper +// +// Copyright (c) 2025 JEngine +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +using System; +using JEngine.UI.Editor.Theming; +using UnityEngine; +using UnityEngine.UIElements; + +namespace JEngine.UI.Editor.Components.Button +{ + /// + /// A two-state toggle button with configurable text and colors. + /// + public class JToggleButton : UnityEngine.UIElements.Button + { + private bool _isOn; + private string _onText; + private string _offText; + private ButtonVariant _onVariant; + private ButtonVariant _offVariant; + private Action _onValueChanged; + + /// + /// Creates a new toggle button. + /// + /// Text when toggle is on. + /// Text when toggle is off. + /// Initial toggle state. + /// Color variant when on. + /// Color variant when off. + /// Callback when value changes. + public JToggleButton( + string onText, + string offText, + bool initialValue = false, + ButtonVariant onVariant = ButtonVariant.Success, + ButtonVariant offVariant = ButtonVariant.Danger, + Action onValueChanged = null) + { + _onText = onText; + _offText = offText; + _onVariant = onVariant; + _offVariant = offVariant; + _onValueChanged = onValueChanged; + + AddToClassList("j-toggle-button"); + ApplyBaseStyles(); + + // Set initial state + SetValue(initialValue, notify: false); + + // Register click handler + clicked += OnClicked; + + // Register hover events + RegisterCallback(OnMouseEnter); + RegisterCallback(OnMouseLeave); + } + + /// + /// Gets or sets the toggle value. + /// + public bool Value + { + get => _isOn; + set => SetValue(value, notify: true); + } + + /// + /// Gets or sets the value changed callback. + /// + public Action OnValueChanged + { + get => _onValueChanged; + set => _onValueChanged = value; + } + + private void ApplyBaseStyles() + { + style.borderTopLeftRadius = Tokens.BorderRadius.Sm; + style.borderTopRightRadius = Tokens.BorderRadius.Sm; + style.borderBottomLeftRadius = Tokens.BorderRadius.Sm; + style.borderBottomRightRadius = Tokens.BorderRadius.Sm; + style.paddingTop = 4; + style.paddingRight = 10; + style.paddingBottom = 4; + style.paddingLeft = 10; + // Remove all margins (like JDropdown does) + style.marginLeft = 0; + style.marginRight = 0; + style.marginTop = 0; + style.marginBottom = 0; + style.minHeight = 22; + style.maxHeight = 24; + style.fontSize = 11; + style.unityFontStyleAndWeight = FontStyle.Normal; + // No border for cleaner look + style.borderTopWidth = 0; + style.borderRightWidth = 0; + style.borderBottomWidth = 0; + style.borderLeftWidth = 0; + // Text color set in UpdateVisuals based on variant + // Don't set flexGrow/flexShrink here - let parent control layout + style.overflow = Overflow.Hidden; + style.textOverflow = TextOverflow.Ellipsis; + style.whiteSpace = WhiteSpace.NoWrap; + } + + private void OnClicked() + { + SetValue(!_isOn, notify: true); + } + + /// + /// Sets the toggle value. + /// + /// The new value. + /// Whether to invoke the callback. + public void SetValue(bool value, bool notify = true) + { + _isOn = value; + UpdateVisuals(); + + if (notify) + { + _onValueChanged?.Invoke(_isOn); + } + } + + private void UpdateVisuals() + { + text = _isOn ? _onText : _offText; + var variant = _isOn ? _onVariant : _offVariant; + style.backgroundColor = JTheme.GetButtonColor(variant); + + // Set text color based on variant using theme tokens + style.color = variant switch + { + ButtonVariant.Primary => Tokens.Colors.PrimaryText, + ButtonVariant.Secondary => Tokens.Colors.SecondaryText, + ButtonVariant.Success => Tokens.Colors.PrimaryText, // Same as primary + ButtonVariant.Danger => Tokens.Colors.PrimaryText, // Same as primary + ButtonVariant.Warning => Tokens.Colors.PrimaryText, // Same as primary + _ => Tokens.Colors.PrimaryText + }; + } + + private void OnMouseEnter(MouseEnterEvent evt) + { + if (!enabledSelf) return; + var variant = _isOn ? _onVariant : _offVariant; + style.backgroundColor = JTheme.GetButtonHoverColor(variant); + } + + private void OnMouseLeave(MouseLeaveEvent evt) + { + var variant = _isOn ? _onVariant : _offVariant; + style.backgroundColor = JTheme.GetButtonColor(variant); + } + + /// + /// Sets the on-state text. + /// + /// The text when on. + /// This button for chaining. + public JToggleButton WithOnText(string text) + { + _onText = text; + UpdateVisuals(); + return this; + } + + /// + /// Sets the off-state text. + /// + /// The text when off. + /// This button for chaining. + public JToggleButton WithOffText(string text) + { + _offText = text; + UpdateVisuals(); + return this; + } + + /// + /// Sets the on-state variant. + /// + /// The variant when on. + /// This button for chaining. + public JToggleButton WithOnVariant(ButtonVariant variant) + { + _onVariant = variant; + UpdateVisuals(); + return this; + } + + /// + /// Sets the off-state variant. + /// + /// The variant when off. + /// This button for chaining. + public JToggleButton WithOffVariant(ButtonVariant variant) + { + _offVariant = variant; + UpdateVisuals(); + return this; + } + + /// + /// Sets the button to fill available width. + /// + /// This button for chaining. + public JToggleButton FullWidth() + { + style.flexGrow = 1; + style.maxHeight = 24; + return this; + } + + /// + /// Adds a CSS class. + /// + /// The class name. + /// This button for chaining. + public JToggleButton WithClass(string className) + { + AddToClassList(className); + return this; + } + } +} diff --git a/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Components/Button/JToggleButton.cs.meta b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Components/Button/JToggleButton.cs.meta new file mode 100644 index 00000000..a358c3f3 --- /dev/null +++ b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Components/Button/JToggleButton.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d9baa70cf5e7a47f08bde34d16f6db3b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Components/Feedback.meta b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Components/Feedback.meta new file mode 100644 index 00000000..abc3fa87 --- /dev/null +++ b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Components/Feedback.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 2168cc1ed92bb4f199955f180c5eb4ee +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Components/Feedback/JLogView.cs b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Components/Feedback/JLogView.cs new file mode 100644 index 00000000..ba347a38 --- /dev/null +++ b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Components/Feedback/JLogView.cs @@ -0,0 +1,195 @@ +// JLogView.cs +// +// Author: +// JasonXuDeveloper +// +// Copyright (c) 2025 JEngine +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +using JEngine.UI.Editor.Theming; +using UnityEngine; +using UnityEngine.UIElements; + +namespace JEngine.UI.Editor.Components.Feedback +{ + /// + /// A scrollable log view with optional max lines limit. + /// + public class JLogView : JComponent + { + private readonly ScrollView _scrollView; + private int _maxLines; + private int _currentLineCount; + + /// + /// Creates a new log view with monochrome background. + /// + /// Maximum number of lines to keep (0 = unlimited). + public JLogView(int maxLines = 100) : base("j-log-view") + { + _maxLines = maxLines; + _currentLineCount = 0; + + // Apply theme-aware background + // Dark mode: subtle layer (not too dark) + // Light mode: surface layer (grey) + style.backgroundColor = Tokens.IsDarkTheme + ? Tokens.Colors.BgSubtle + : Tokens.Colors.BgSurface; + + // Standard borders + style.borderTopColor = Tokens.Colors.Border; + style.borderRightColor = Tokens.Colors.Border; + style.borderBottomColor = Tokens.Colors.Border; + style.borderLeftColor = Tokens.Colors.Border; + style.borderTopWidth = 1; + style.borderRightWidth = 1; + style.borderBottomWidth = 1; + style.borderLeftWidth = 1; + + // Border radius (8px) + style.borderTopLeftRadius = Tokens.BorderRadius.MD; + style.borderTopRightRadius = Tokens.BorderRadius.MD; + style.borderBottomLeftRadius = Tokens.BorderRadius.MD; + style.borderBottomRightRadius = Tokens.BorderRadius.MD; + + style.minHeight = 100; + style.maxHeight = 300; + style.paddingTop = Tokens.Spacing.MD; + style.paddingRight = Tokens.Spacing.MD; + style.paddingBottom = Tokens.Spacing.MD; + style.paddingLeft = Tokens.Spacing.MD; + + // Create scroll view + _scrollView = new ScrollView(ScrollViewMode.VerticalAndHorizontal); + _scrollView.AddToClassList("j-log-view__scroll"); + _scrollView.style.flexGrow = 1; + _scrollView.horizontalScrollerVisibility = ScrollerVisibility.Auto; + _scrollView.verticalScrollerVisibility = ScrollerVisibility.Auto; + Add(_scrollView); + } + + /// + /// Gets the scroll view container. + /// + public ScrollView ScrollView => _scrollView; + + /// + /// Gets or sets the maximum number of lines. + /// + public int MaxLines + { + get => _maxLines; + set => _maxLines = value; + } + + /// + /// Logs an info message. + /// + /// The message to log. + /// This log view for chaining. + public JLogView LogInfo(string message) + { + return Log(message); + } + + /// + /// Logs an error message. + /// + /// The message to log. + /// This log view for chaining. + public JLogView LogError(string message) + { + return Log(message, true); + } + + /// + /// Logs a message. + /// + /// The message to log. + /// Whether this is an error message. + /// This log view for chaining. + public JLogView Log(string message, bool isError = false) + { + var entry = new Label(message); + entry.AddToClassList("j-log-view__entry"); + entry.AddToClassList(isError ? "j-log-view__entry--error" : "j-log-view__entry--info"); + + entry.style.fontSize = Tokens.FontSize.Sm; + entry.style.paddingTop = Tokens.Spacing.Xs; + entry.style.paddingBottom = Tokens.Spacing.Xs; + entry.style.borderBottomColor = new Color(1, 1, 1, 0.05f); + entry.style.borderBottomWidth = 1; + entry.style.color = isError ? Tokens.Colors.StatusError : Tokens.Colors.TextSecondary; + entry.style.whiteSpace = WhiteSpace.Normal; + + _scrollView.Add(entry); + _currentLineCount++; + + // Remove oldest entries if over limit + if (_maxLines > 0 && _currentLineCount > _maxLines) + { + while (_scrollView.childCount > _maxLines) + { + _scrollView.RemoveAt(0); + _currentLineCount--; + } + } + + // Scroll to bottom + _scrollView.ScrollTo(entry); + + return this; + } + + /// + /// Clears all log entries. + /// + /// This log view for chaining. + public new JLogView Clear() + { + _scrollView.Clear(); + _currentLineCount = 0; + return this; + } + + /// + /// Sets the minimum height. + /// + /// The minimum height. + /// This log view for chaining. + public JLogView WithMinHeight(float height) + { + style.minHeight = height; + return this; + } + + /// + /// Sets the maximum height. + /// + /// The maximum height. + /// This log view for chaining. + public JLogView WithMaxHeight(float height) + { + style.maxHeight = height; + return this; + } + } +} diff --git a/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Components/Feedback/JLogView.cs.meta b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Components/Feedback/JLogView.cs.meta new file mode 100644 index 00000000..b0d2ee15 --- /dev/null +++ b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Components/Feedback/JLogView.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b5378b44edd534b5fa7068e843f74996 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Components/Feedback/JProgressBar.cs b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Components/Feedback/JProgressBar.cs new file mode 100644 index 00000000..19a6214a --- /dev/null +++ b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Components/Feedback/JProgressBar.cs @@ -0,0 +1,155 @@ +// JProgressBar.cs +// +// Author: +// JasonXuDeveloper +// +// Copyright (c) 2025 JEngine +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +using JEngine.UI.Editor.Theming; +using UnityEngine; +using UnityEngine.UIElements; + +namespace JEngine.UI.Editor.Components.Feedback +{ + /// + /// A progress bar indicator. + /// + public class JProgressBar : JComponent + { + private readonly VisualElement _fill; + private float _progress; + private bool _useSuccessColor; + + /// + /// Creates a new progress bar. + /// + /// Initial progress value (0-1). + public JProgressBar(float initialProgress = 0f) : base("j-progress-bar") + { + // Apply container styles + style.height = 8; + style.backgroundColor = Tokens.Colors.BgSurface; + style.borderTopLeftRadius = Tokens.BorderRadius.Sm; + style.borderTopRightRadius = Tokens.BorderRadius.Sm; + style.borderBottomLeftRadius = Tokens.BorderRadius.Sm; + style.borderBottomRightRadius = Tokens.BorderRadius.Sm; + style.overflow = Overflow.Hidden; + + // Create fill element + _fill = new VisualElement(); + _fill.AddToClassList("j-progress-bar__fill"); + _fill.style.height = Length.Percent(100); + _fill.style.backgroundColor = Tokens.Colors.Primary; + _fill.style.borderTopLeftRadius = Tokens.BorderRadius.Sm; + _fill.style.borderTopRightRadius = Tokens.BorderRadius.Sm; + _fill.style.borderBottomLeftRadius = Tokens.BorderRadius.Sm; + _fill.style.borderBottomRightRadius = Tokens.BorderRadius.Sm; + Add(_fill); + + SetProgress(initialProgress); + } + + /// + /// Gets the fill element. + /// + public VisualElement Fill => _fill; + + /// + /// Gets or sets the progress value (0-1). + /// + public float Progress + { + get => _progress; + set => SetProgress(value); + } + + /// + /// Sets the progress value. + /// + /// Progress value (0-1). + /// This progress bar for chaining. + public JProgressBar SetProgress(float value) + { + _progress = Mathf.Clamp01(value); + _fill.style.width = Length.Percent(_progress * 100); + return this; + } + + /// + /// Uses success color when progress reaches 100%. + /// + /// Whether to use success color. + /// This progress bar for chaining. + public JProgressBar WithSuccessOnComplete(bool useSuccess = true) + { + _useSuccessColor = useSuccess; + UpdateFillColor(); + return this; + } + + private void UpdateFillColor() + { + if (_useSuccessColor && _progress >= 1f) + { + _fill.style.backgroundColor = Tokens.Colors.Success; + _fill.AddToClassList("j-progress-bar__fill--success"); + } + else + { + _fill.style.backgroundColor = Tokens.Colors.Primary; + _fill.RemoveFromClassList("j-progress-bar__fill--success"); + } + } + + /// + /// Sets the height of the progress bar. + /// + /// The height in pixels. + /// This progress bar for chaining. + public JProgressBar WithHeight(float height) + { + style.height = height; + return this; + } + + /// + /// Sets the fill color. + /// + /// The fill color. + /// This progress bar for chaining. + public JProgressBar WithColor(Color color) + { + _fill.style.backgroundColor = color; + return this; + } + + /// + /// Sets the fill color using a button variant. + /// + /// The variant to use for color. + /// This progress bar for chaining. + public JProgressBar WithVariant(ButtonVariant variant) + { + _fill.style.backgroundColor = JTheme.GetButtonColor(variant); + return this; + } + } +} diff --git a/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Components/Feedback/JProgressBar.cs.meta b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Components/Feedback/JProgressBar.cs.meta new file mode 100644 index 00000000..2c8f4937 --- /dev/null +++ b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Components/Feedback/JProgressBar.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8797e2997e1494f0ba2c765a6db28f72 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Components/Feedback/JStatusBar.cs b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Components/Feedback/JStatusBar.cs new file mode 100644 index 00000000..f933d969 --- /dev/null +++ b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Components/Feedback/JStatusBar.cs @@ -0,0 +1,147 @@ +// JStatusBar.cs +// +// Author: +// JasonXuDeveloper +// +// Copyright (c) 2025 JEngine +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +using JEngine.UI.Editor.Theming; +using UnityEngine; +using UnityEngine.UIElements; + +namespace JEngine.UI.Editor.Components.Feedback +{ + /// + /// A status bar with colored accent based on status type. + /// + public class JStatusBar : JComponent + { + private readonly Label _textLabel; + private StatusType _status; + + /// + /// Creates a new status bar with neutral monochrome styling. + /// + /// The status text. + /// The status type (Info, Success, Warning, Error). + public JStatusBar(string text = "", StatusType status = StatusType.Info) : base("j-status-bar") + { + style.flexDirection = FlexDirection.Row; + style.alignItems = Align.Center; + style.paddingTop = Tokens.Spacing.MD; + style.paddingRight = Tokens.Spacing.Lg; + style.paddingBottom = Tokens.Spacing.MD; + style.paddingLeft = Tokens.Spacing.Lg; + + // Border radius (8px) + style.borderTopLeftRadius = Tokens.BorderRadius.MD; + style.borderTopRightRadius = Tokens.BorderRadius.MD; + style.borderBottomLeftRadius = Tokens.BorderRadius.MD; + style.borderBottomRightRadius = Tokens.BorderRadius.MD; + + style.marginBottom = Tokens.Spacing.MD; + + // Thick accent border on left (3px) + style.borderLeftWidth = 3; + + _textLabel = new Label(text); + _textLabel.AddToClassList("j-status-bar__text"); + _textLabel.style.color = Tokens.Colors.TextPrimary; + _textLabel.style.fontSize = Tokens.FontSize.Base; + Add(_textLabel); + + SetStatus(status); + } + + /// + /// Gets the text label. + /// + public Label TextLabel => _textLabel; + + /// + /// Gets or sets the status text. + /// + public string Text + { + get => _textLabel.text; + set => _textLabel.text = value; + } + + /// + /// Gets or sets the status type. + /// + public StatusType Status + { + get => _status; + set => SetStatus(value); + } + + /// + /// Sets the status type. + /// + /// The status type. + /// This status bar for chaining. + public JStatusBar SetStatus(StatusType status) + { + // Remove existing status classes + RemoveFromClassList("j-status-bar--info"); + RemoveFromClassList("j-status-bar--success"); + RemoveFromClassList("j-status-bar--warning"); + RemoveFromClassList("j-status-bar--error"); + + _status = status; + + // Monochrome design: neutral grey backgrounds in both themes + Color bgColor; + Color accentColor; + string statusClass; + + // Monochrome design: neutral grey backgrounds and borders in both themes + var neutralBg = Tokens.Colors.BgSurface; + var neutralBorder = Tokens.Colors.Border; + (bgColor, accentColor, statusClass) = status switch + { + StatusType.Info => (neutralBg, neutralBorder, "j-status-bar--info"), + StatusType.Success => (neutralBg, neutralBorder, "j-status-bar--success"), + StatusType.Warning => (neutralBg, neutralBorder, "j-status-bar--warning"), + StatusType.Error => (neutralBg, neutralBorder, "j-status-bar--error"), + _ => (neutralBg, neutralBorder, "j-status-bar--info") + }; + + AddToClassList(statusClass); + style.backgroundColor = bgColor; + style.borderLeftColor = accentColor; + + return this; + } + + /// + /// Sets the status text. + /// + /// The new text. + /// This status bar for chaining. + public JStatusBar WithText(string text) + { + _textLabel.text = text; + return this; + } + } +} diff --git a/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Components/Feedback/JStatusBar.cs.meta b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Components/Feedback/JStatusBar.cs.meta new file mode 100644 index 00000000..9a1ac8bb --- /dev/null +++ b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Components/Feedback/JStatusBar.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 02072868fc82a4625aa35b04bf9a8a75 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Components/Form.meta b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Components/Form.meta new file mode 100644 index 00000000..222098c7 --- /dev/null +++ b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Components/Form.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: c3c03880e7c9a4146b3151debdae422e +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Components/Form/JDropdown.cs b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Components/Form/JDropdown.cs new file mode 100644 index 00000000..3b9b4538 --- /dev/null +++ b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Components/Form/JDropdown.cs @@ -0,0 +1,145 @@ +// JDropdown.cs +// +// Author: +// JasonXuDeveloper +// +// Copyright (c) 2025 JEngine +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +using System; +using System.Collections.Generic; +using JEngine.UI.Editor.Utilities; +using UnityEngine.UIElements; + +namespace JEngine.UI.Editor.Components.Form +{ + /// + /// A styled dropdown matching the JEngine dark theme. + /// + public class JDropdown : VisualElement + { + private readonly PopupField _popupField; + private Action _onValueChanged; + private bool _stylesApplied; + + /// + /// Creates a new styled dropdown. + /// + /// Available choices. + /// Default selected value. + public JDropdown(List choices, string defaultValue = null) + { + AddToClassList("j-dropdown"); + + // Ensure choices list is valid + if (choices == null || choices.Count == 0) + { + choices = new List { "" }; + } + + // Determine initial index + int defaultIndex = 0; + if (!string.IsNullOrEmpty(defaultValue) && choices.Contains(defaultValue)) + { + defaultIndex = choices.IndexOf(defaultValue); + } + + _popupField = new PopupField(choices, defaultIndex); + + // Style the container + style.flexGrow = 1; + style.flexShrink = 1; + style.minWidth = 80; + style.minHeight = 20; + style.maxHeight = 24; + style.alignSelf = Align.Center; + + // Apply styles + _popupField.style.flexGrow = 1; + _popupField.style.marginLeft = 0; + _popupField.style.marginRight = 0; + _popupField.style.marginTop = 0; + _popupField.style.marginBottom = 0; + + Add(_popupField); + + // Apply styles using GeometryChangedEvent (fires after layout) + _popupField.RegisterCallback(OnGeometryChanged); + } + + private void OnGeometryChanged(GeometryChangedEvent evt) + { + // Apply styles only once + if (_stylesApplied) return; + + // Unregister to prevent multiple calls + _popupField.UnregisterCallback(OnGeometryChanged); + + // Use PopupFieldStyleHelper for reliable styling + PopupFieldStyleHelper.ApplyStylesToPopupField( + _popupField, + enableHoverEffects: true, + enableDebugLogging: false); // Set to true for debugging + + _stylesApplied = true; + } + + /// + /// Gets or sets the selected value. + /// + public string Value + { + get => _popupField.value; + set => _popupField.value = value; + } + + /// + /// Gets the internal PopupField for advanced operations. + /// + public PopupField PopupField => _popupField; + + /// + /// Gets or sets the choices. + /// + public List Choices + { + get => _popupField.choices; + set => _popupField.choices = value; + } + + /// + /// Registers a callback for value changes. + /// + public void RegisterValueChangedCallback(EventCallback> callback) + { + _popupField.RegisterValueChangedCallback(callback); + } + + /// + /// Sets the value changed callback. + /// + public JDropdown OnValueChanged(Action callback) + { + _onValueChanged = callback; + _popupField.RegisterValueChangedCallback(evt => _onValueChanged?.Invoke(evt.newValue)); + return this; + } + } +} diff --git a/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Components/Form/JDropdown.cs.meta b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Components/Form/JDropdown.cs.meta new file mode 100644 index 00000000..75e6af04 --- /dev/null +++ b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Components/Form/JDropdown.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 68ac9823206544251bb16999e0d65c85 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Components/Form/JFormField.cs b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Components/Form/JFormField.cs new file mode 100644 index 00000000..3a7a45af --- /dev/null +++ b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Components/Form/JFormField.cs @@ -0,0 +1,159 @@ +// JFormField.cs +// +// Author: +// JasonXuDeveloper +// +// Copyright (c) 2025 JEngine +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +using JEngine.UI.Editor.Theming; +using UnityEngine; +using UnityEngine.UIElements; + +namespace JEngine.UI.Editor.Components.Form +{ + /// + /// A form field with label and control in a two-column layout. + /// + public class JFormField : JComponent + { + private readonly Label _label; + private readonly VisualElement _controlContainer; + + /// + /// Creates a new form field with a label and control. + /// + /// The label text. + /// The control element (TextField, PopupField, etc.). + public JFormField(string labelText, VisualElement control = null) : base("j-form-field") + { + style.flexDirection = FlexDirection.Row; + style.alignItems = Align.Center; + style.flexWrap = Wrap.NoWrap; + style.marginBottom = Tokens.Spacing.Sm; + style.minHeight = 22; + style.maxHeight = 28; + style.overflow = Overflow.Hidden; + + // Create label - compact + _label = new Label(labelText); + _label.AddToClassList("j-form-field__label"); + _label.style.width = Tokens.Layout.FormLabelWidth; + _label.style.minWidth = Tokens.Layout.FormLabelMinWidth; + _label.style.maxWidth = 180; + _label.style.color = Tokens.Colors.TextSecondary; + _label.style.fontSize = 11; + _label.style.paddingRight = Tokens.Spacing.Sm; + _label.style.flexShrink = 1; + _label.style.overflow = Overflow.Hidden; + _label.style.textOverflow = TextOverflow.Ellipsis; + _label.style.whiteSpace = WhiteSpace.NoWrap; + _label.style.unityTextAlign = TextAnchor.MiddleLeft; + base.Add(_label); + + // Create control container + _controlContainer = new VisualElement(); + _controlContainer.AddToClassList("j-form-field__control"); + _controlContainer.style.flexGrow = 1; + _controlContainer.style.flexShrink = 1; + _controlContainer.style.flexDirection = FlexDirection.Row; + _controlContainer.style.alignItems = Align.Center; + _controlContainer.style.minWidth = 80; + _controlContainer.style.overflow = Overflow.Hidden; + base.Add(_controlContainer); + + // Add control if provided + if (control != null) + { + // Only JToggle should be fixed-size, all other controls (including buttons) should be responsive + bool isFixedSize = control is JToggle; + if (!isFixedSize) + { + control.style.flexGrow = 1; + control.style.flexShrink = 1; + } + control.style.alignSelf = Align.Center; + _controlContainer.Add(control); + } + } + + /// + /// Gets the label element. + /// + public Label Label => _label; + + /// + /// Gets the control container. + /// + public VisualElement ControlContainer => _controlContainer; + + /// + /// Sets the control for this form field. + /// + /// The control element. + /// This form field for chaining. + public JFormField WithControl(VisualElement control) + { + _controlContainer.Clear(); + if (control != null) + { + _controlContainer.Add(control); + } + return this; + } + + /// + /// Sets the label width. + /// + /// The label width in pixels. + /// This form field for chaining. + public JFormField WithLabelWidth(float width) + { + _label.style.width = width; + _label.style.minWidth = width; + return this; + } + + /// + /// Hides the label (for full-width controls). + /// + /// This form field for chaining. + public JFormField NoLabel() + { + _label.style.display = DisplayStyle.None; + return this; + } + + /// + /// Adds child elements to the control container. + /// + public new JFormField Add(params VisualElement[] children) + { + foreach (var child in children) + { + if (child != null) + { + _controlContainer.Add(child); + } + } + return this; + } + } +} diff --git a/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Components/Form/JFormField.cs.meta b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Components/Form/JFormField.cs.meta new file mode 100644 index 00000000..09868f42 --- /dev/null +++ b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Components/Form/JFormField.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c15853bd465ac42908ff76268dafbed6 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Components/Form/JFormSection.cs b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Components/Form/JFormSection.cs new file mode 100644 index 00000000..6929d3e0 --- /dev/null +++ b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Components/Form/JFormSection.cs @@ -0,0 +1,129 @@ +// JFormSection.cs +// +// Author: +// JasonXuDeveloper +// +// Copyright (c) 2025 JEngine +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +using JEngine.UI.Editor.Theming; +using UnityEngine; +using UnityEngine.UIElements; + +namespace JEngine.UI.Editor.Components.Form +{ + /// + /// A section containing grouped form fields with an optional header. + /// + public class JFormSection : JComponent + { + private readonly Label _header; + private readonly VisualElement _fields; + + /// + /// Creates a new form section with an optional header. + /// + /// The section header text, or null for no header. + public JFormSection(string headerText = null) : base("j-form-section") + { + style.marginBottom = Tokens.Spacing.Lg; + + // Create header if provided + if (!string.IsNullOrEmpty(headerText)) + { + _header = new Label(headerText); + _header.AddToClassList("j-form-section__header"); + _header.style.color = Tokens.Colors.TextMuted; + _header.style.fontSize = Tokens.FontSize.Sm; + _header.style.unityFontStyleAndWeight = FontStyle.Bold; + _header.style.marginBottom = Tokens.Spacing.MD; + base.Add(_header); + } + + // Create fields container + _fields = new VisualElement(); + _fields.style.flexDirection = FlexDirection.Column; + base.Add(_fields); + } + + /// + /// Gets the header label. + /// + public Label Header => _header; + + /// + /// Gets the fields container. + /// + public VisualElement Fields => _fields; + + /// + /// Adds a form field with label and control. + /// + /// The field label. + /// The control element. + /// This form section for chaining. + public JFormSection AddField(string label, VisualElement control) + { + var field = new JFormField(label, control); + _fields.Add(field); + return this; + } + + /// + /// Adds a pre-built form field. + /// + /// The form field to add. + /// This form section for chaining. + public JFormSection AddField(JFormField field) + { + _fields.Add(field); + return this; + } + + /// + /// Adds child elements to the fields container. + /// + public new JFormSection Add(params VisualElement[] children) + { + foreach (var child in children) + { + if (child != null) + { + _fields.Add(child); + } + } + return this; + } + + /// + /// Sets the header text. + /// + /// The new header text. + /// This form section for chaining. + public JFormSection WithHeader(string text) + { + if (_header != null) + { + _header.text = text; + } + return this; + } + } +} diff --git a/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Components/Form/JFormSection.cs.meta b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Components/Form/JFormSection.cs.meta new file mode 100644 index 00000000..045f454c --- /dev/null +++ b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Components/Form/JFormSection.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9ee6d4507273b4da79e1f9a1b962962c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Components/Form/JObjectField.cs b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Components/Form/JObjectField.cs new file mode 100644 index 00000000..9af422c9 --- /dev/null +++ b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Components/Form/JObjectField.cs @@ -0,0 +1,330 @@ +// JObjectField.cs +// +// Author: +// JasonXuDeveloper +// +// Copyright (c) 2025 JEngine +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +using System; +using JEngine.UI.Editor.Theming; +using UnityEditor; +using UnityEditor.UIElements; +using UnityEngine; +using UnityEngine.UIElements; +using Object = UnityEngine.Object; + +namespace JEngine.UI.Editor.Components.Form +{ + /// + /// A styled object field matching the JEngine dark theme. + /// + public class JObjectField : VisualElement + { + private readonly ObjectField _objectField; + + /// + /// Creates a new styled object field. + /// + /// The type of object to accept. + /// Whether to allow scene objects. + public JObjectField(Type objectType, bool allowSceneObjects = true) + { + AddToClassList("j-object-field"); + + _objectField = new ObjectField + { + objectType = objectType, + allowSceneObjects = allowSceneObjects + }; + + // Style the container + style.flexGrow = 1; + style.flexShrink = 1; + style.minWidth = 80; + style.minHeight = 20; + style.maxHeight = 24; + style.alignSelf = Align.Center; + + _objectField.style.flexGrow = 1; + _objectField.style.marginLeft = 0; + _objectField.style.marginRight = 0; + _objectField.style.marginTop = 0; + _objectField.style.marginBottom = 0; + _objectField.style.height = 22; + + _objectField.RegisterCallback(OnAttachToPanel); + + Add(_objectField); + } + + private void ApplyInternalStyles() + { + // Style the input container + var input = _objectField.Q(className: "unity-object-field__input"); + if (input != null) + { + input.style.backgroundColor = Tokens.Colors.BgInput; + input.style.borderTopColor = Tokens.Colors.BorderSubtle; + input.style.borderRightColor = Tokens.Colors.BorderSubtle; + input.style.borderBottomColor = Tokens.Colors.BorderSubtle; + input.style.borderLeftColor = Tokens.Colors.BorderSubtle; + input.style.borderTopWidth = 1; + input.style.borderRightWidth = 1; + input.style.borderBottomWidth = 1; + input.style.borderLeftWidth = 1; + input.style.borderTopLeftRadius = Tokens.BorderRadius.Sm; + input.style.borderTopRightRadius = Tokens.BorderRadius.Sm; + input.style.borderBottomLeftRadius = Tokens.BorderRadius.Sm; + input.style.borderBottomRightRadius = Tokens.BorderRadius.Sm; + input.style.paddingLeft = Tokens.Spacing.Sm; + input.style.paddingRight = Tokens.Spacing.Sm; + input.style.minHeight = 22; + input.style.height = 22; + input.style.alignItems = Align.Center; + + // Hover effect + input.RegisterCallback(static (_, element) => + { + element.style.backgroundColor = Tokens.Colors.BgHover; + }, input); + + input.RegisterCallback(static (_, element) => + { + element.style.backgroundColor = Tokens.Colors.BgInput; + }, input); + } + + // Style the object display container (icon + label) + var display = _objectField.Q(className: "unity-object-field__display"); + if (display != null) + { + display.style.alignItems = Align.Center; + display.style.height = 20; + } + + // Style the object label - ensure vertical centering + var objectLabel = _objectField.Q(className: "unity-object-field-display__label"); + if (objectLabel != null) + { + objectLabel.style.color = Tokens.Colors.TextPrimary; + objectLabel.style.fontSize = Tokens.FontSize.Sm; + objectLabel.style.unityTextAlign = TextAnchor.MiddleLeft; + objectLabel.style.alignSelf = Align.Center; + } + + // Style the icon + var icon = _objectField.Q(className: "unity-object-field-display__icon"); + if (icon != null) + { + icon.style.alignSelf = Align.Center; + } + + // Hide the field label if present + var label = _objectField.Q