using System.Collections.Generic; using System.IO; using Uigenx.Protocol; using UnityEditor; using UnityEngine; using UnityEngine.UIElements; namespace Uigenx.Editor { /// /// Turns the manifest's panels into PanelSettings assets. /// /// The compiler cannot write one: a PanelSettings is a serialised Unity object holding an asset /// reference to a theme, so only the editor can produce one that survives a re-import. Leaving /// it to the user is worse than it sounds, because the default Unity gives a new panel is /// constant pixel size, and a HUD authored at 1920x1080 then only looks right at 1920x1080. /// /// /// The studio owns the fields the plan carries and rewrites them on every import. Anything the /// plan does not describe, a target texture or the clear colour, is left exactly as it was. /// public static class UgxPanelBuilder { /// Where Unity's own generated runtime theme lives, so this does not create a second one. public const string DefaultThemePath = "Assets/UI Toolkit/UnityThemes/UnityDefaultTheme.tss"; /// The one URL the TSS importer accepts for the theme built into the engine. const string DefaultThemeSource = "@import url(\"unity-theme://default\");\n"; public sealed class Result { public List Panels { get; } = new List(); public List Warnings { get; } = new List(); /// The panel a project with no choice of its own should draw on: the lowest sort order. public PanelSettings Default { get; internal set; } } public static Result Build(IReadOnlyList panels, string assetsUiFolder) { var result = new Result(); if (panels == null || panels.Count == 0) return result; if (string.IsNullOrEmpty(assetsUiFolder)) assetsUiFolder = UgxProjectSettings.Load().AssetsUiFolder; float lowest = float.MaxValue; foreach (var plan in panels) { if (plan == null || string.IsNullOrEmpty(plan.Path)) continue; var panel = BuildOne(plan, assetsUiFolder, result.Warnings); if (panel == null) continue; result.Panels.Add(panel); if (plan.SortOrder >= lowest) continue; lowest = plan.SortOrder; result.Default = panel; } if (result.Panels.Count > 0) AssetDatabase.SaveAssets(); return result; } static PanelSettings BuildOne(UgxManifestPanel plan, string assetsUiFolder, List warnings) { var path = UgxBundleImporter.CombineAssetPath(assetsUiFolder, plan.Path); var panel = AssetDatabase.LoadAssetAtPath(path); if (panel == null) { EnsureFolder(Path.GetDirectoryName(path).Replace('\\', '/')); panel = ScriptableObject.CreateInstance(); AssetDatabase.CreateAsset(panel, path); } panel.scaleMode = ScaleMode(plan.ScaleMode); panel.scale = plan.Scale; panel.referenceDpi = plan.ReferenceDpi; panel.fallbackDpi = plan.FallbackDpi; panel.referenceResolution = Resolution(plan, warnings); panel.screenMatchMode = MatchMode(plan.ScreenMatchMode); panel.match = plan.Match; panel.referenceSpritePixelsPerUnit = plan.ReferenceSpritePixelsPerUnit; panel.sortingOrder = plan.SortOrder; var theme = ResolveTheme(plan, assetsUiFolder, panel, warnings); if (theme != null) panel.themeStyleSheet = theme; EditorUtility.SetDirty(panel); return panel; } /// /// The theme the plan names, or the engine's default. A theme the project already chose is /// never replaced by the default: that choice is not the export's to make. /// static ThemeStyleSheet ResolveTheme(UgxManifestPanel plan, string assetsUiFolder, PanelSettings panel, List warnings) { if (!string.IsNullOrEmpty(plan.Theme)) { var themePath = UgxBundleImporter.CombineAssetPath(assetsUiFolder, plan.Theme); var named = AssetDatabase.LoadAssetAtPath(themePath); if (named != null) return named; warnings.Add(plan.Name + ": no theme at " + themePath + ", using the default theme"); } return panel.themeStyleSheet != null ? null : EnsureDefaultTheme(warnings); } /// /// Unity writes this file itself, but only when a UIDocument is added to a GameObject by /// hand. A panel built from an import would otherwise have no theme at all, which draws the /// elements and none of the controls. /// public static ThemeStyleSheet EnsureDefaultTheme(List warnings) { var existing = AssetDatabase.LoadAssetAtPath(DefaultThemePath); if (existing != null) return existing; Directory.CreateDirectory(Path.GetDirectoryName(DefaultThemePath)); File.WriteAllText(DefaultThemePath, DefaultThemeSource); AssetDatabase.ImportAsset(DefaultThemePath, ImportAssetOptions.ForceSynchronousImport); var created = AssetDatabase.LoadAssetAtPath(DefaultThemePath); if (created == null) warnings.Add(DefaultThemePath + " was written but did not import as a theme"); return created; } static Vector2Int Resolution(UgxManifestPanel plan, List warnings) { var resolution = plan.ReferenceResolution; if (resolution != null && resolution.Length >= 2 && resolution[0] > 0 && resolution[1] > 0) return new Vector2Int(resolution[0], resolution[1]); warnings.Add(plan.Name + ": no reference resolution, keeping 1920x1080"); return new Vector2Int(1920, 1080); } static PanelScaleMode ScaleMode(string wireName) { switch (wireName) { case "constant-pixel-size": return PanelScaleMode.ConstantPixelSize; case "constant-physical-size": return PanelScaleMode.ConstantPhysicalSize; default: return PanelScaleMode.ScaleWithScreenSize; } } static PanelScreenMatchMode MatchMode(string wireName) { switch (wireName) { case "shrink": return PanelScreenMatchMode.Shrink; case "expand": return PanelScreenMatchMode.Expand; default: return PanelScreenMatchMode.MatchWidthOrHeight; } } static void EnsureFolder(string folder) { if (string.IsNullOrEmpty(folder) || AssetDatabase.IsValidFolder(folder)) return; var parts = folder.Split('/'); var current = parts[0]; for (int i = 1; i < parts.Length; i++) { var next = current + "/" + parts[i]; if (!AssetDatabase.IsValidFolder(next)) AssetDatabase.CreateFolder(current, parts[i]); current = next; } } } }