using System; using System.Collections.Generic; using System.IO; using Newtonsoft.Json; using UnityEditor; using UnityEngine; using UnityEngine.UIElements; namespace Uigenx.Editor { /// /// Instantiates every built in control in a runtime panel with the project's default theme and /// writes what Unity actually produced: the internal part hierarchy, the USS classes on each /// part, and the resolved default style values. The studio reads this dump so its canvas draws /// controls the way Unity does instead of guessing. /// public static class UgxThemeDumper { /// Command line form: `-ugx-theme-dump <path>`, used by CI. public const string PathArgument = "-ugx-theme-dump"; /// Editor updates to wait for the runtime panel to lay out before reading it. public const int LayoutFrames = 4; /// Depth cap on the part walk. Built in controls nest at most a few levels. public const int MaxDepth = 8; public static void Dump(string outputPath, Action onDone = null) { if (string.IsNullOrEmpty(outputPath)) throw new ArgumentException("output path is required"); var template = UgxSettings.Instance.DefaultPanelSettings; if (template == null) { Debug.LogError("[ugx] the theme dump needs a default PanelSettings in UgxSettings so it can use " + "the project's runtime theme"); return; } var panel = UnityEngine.Object.Instantiate(template); panel.name = "ugx-theme-dump-panel"; panel.hideFlags = HideFlags.HideAndDontSave; panel.scaleMode = PanelScaleMode.ConstantPixelSize; panel.sortingOrder = -10_000f; var host = new GameObject("ugx-theme-dump") { hideFlags = HideFlags.HideAndDontSave }; var document = host.AddComponent(); document.panelSettings = panel; var root = document.rootVisualElement; root.style.width = 1024f; root.style.height = 4096f; var controls = BuildControls(); foreach (var control in controls) root.Add(control.Value); int framesLeft = LayoutFrames; void Step() { if (--framesLeft > 0) return; EditorApplication.update -= Step; try { var dump = new ThemeDump { UnityVersion = Application.unityVersion, Theme = panel.themeStyleSheet != null ? panel.themeStyleSheet.name : "unknown", }; foreach (var control in controls) dump.Controls.Add(Describe(control.Key, control.Value)); Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(outputPath)) ?? "."); File.WriteAllText(outputPath, JsonConvert.SerializeObject(dump, Formatting.Indented) + "\n"); Debug.Log("[ugx] wrote the theme dump to " + outputPath); onDone?.Invoke(outputPath); } catch (Exception e) { Debug.LogError("[ugx] the theme dump failed: " + e); } finally { UnityEngine.Object.DestroyImmediate(host); UnityEngine.Object.DestroyImmediate(panel); } } EditorApplication.update += Step; } [InitializeOnLoadMethod] static void DumpFromCommandLine() { var args = Environment.GetCommandLineArgs(); for (int i = 0; i < args.Length - 1; i++) { if (args[i] != PathArgument) continue; var path = args[i + 1]; EditorApplication.delayCall += () => Dump(path, _ => { if (Application.isBatchMode) EditorApplication.Exit(0); }); return; } } /// Every built in control the studio can place. Editor only controls are left out. static List> BuildControls() { var list = new List>(24); void Add(string name, VisualElement element) => list.Add(new KeyValuePair(name, element)); var dropdown = new DropdownField { label = "Dropdown", choices = new List { "One", "Two" } }; dropdown.index = 0; Add("VisualElement", new VisualElement()); Add("Label", new Label("Label")); Add("Button", new Button { text = "Button" }); Add("Toggle", new Toggle { label = "Toggle" }); Add("TextField", new TextField { label = "TextField", value = "text" }); Add("Slider", new Slider { label = "Slider", lowValue = 0f, highValue = 1f, value = 0.5f }); Add("SliderInt", new SliderInt { label = "SliderInt", lowValue = 0, highValue = 10, value = 5 }); Add("MinMaxSlider", new MinMaxSlider("MinMaxSlider", 0.2f, 0.8f, 0f, 1f)); Add("DropdownField", dropdown); Add("Foldout", new Foldout { text = "Foldout" }); Add("ScrollView", new ScrollView()); Add("ListView", new ListView()); Add("ProgressBar", new ProgressBar { value = 50f }); Add("RadioButton", new RadioButton { label = "RadioButton" }); Add("RadioButtonGroup", new RadioButtonGroup("Group", new List { "A", "B" })); Add("GroupBox", new GroupBox("GroupBox")); Add("HelpBox", new HelpBox("HelpBox", HelpBoxMessageType.Info)); Add("IntegerField", new IntegerField { label = "IntegerField" }); Add("FloatField", new FloatField { label = "FloatField" }); Add("Vector2Field", new Vector2Field { label = "Vector2Field" }); Add("Vector3Field", new Vector3Field { label = "Vector3Field" }); Add("Scroller", new Scroller()); return list; } static ControlDump Describe(string name, VisualElement element) => new ControlDump { Control = name, Root = DescribePart(element, 0) }; static PartDump DescribePart(VisualElement element, int depth) { var resolved = element.resolvedStyle; var part = new PartDump { Name = element.name, Type = element.GetType().Name, Classes = new List(element.GetClasses()), Style = new Dictionary { ["width"] = Num(resolved.width), ["height"] = Num(resolved.height), ["flex-direction"] = resolved.flexDirection.ToString(), ["flex-grow"] = Num(resolved.flexGrow), ["flex-shrink"] = Num(resolved.flexShrink), ["align-items"] = resolved.alignItems.ToString(), ["justify-content"] = resolved.justifyContent.ToString(), ["padding"] = Box(resolved.paddingTop, resolved.paddingRight, resolved.paddingBottom, resolved.paddingLeft), ["margin"] = Box(resolved.marginTop, resolved.marginRight, resolved.marginBottom, resolved.marginLeft), ["border-width"] = Box(resolved.borderTopWidth, resolved.borderRightWidth, resolved.borderBottomWidth, resolved.borderLeftWidth), ["border-radius"] = Box(resolved.borderTopLeftRadius, resolved.borderTopRightRadius, resolved.borderBottomRightRadius, resolved.borderBottomLeftRadius), ["background-color"] = Hex(resolved.backgroundColor), ["color"] = Hex(resolved.color), ["border-color"] = Hex(resolved.borderTopColor), ["font-size"] = Num(resolved.fontSize), ["-unity-font-style"] = resolved.unityFontStyleAndWeight.ToString(), ["-unity-text-align"] = resolved.unityTextAlign.ToString(), ["opacity"] = Num(resolved.opacity), }, }; if (depth >= MaxDepth) return part; for (int i = 0; i < element.childCount; i++) part.Children.Add(DescribePart(element[i], depth + 1)); return part; } static string Num(float value) => float.IsNaN(value) ? "auto" : value.ToString("0.###", System.Globalization.CultureInfo.InvariantCulture); static string Box(float top, float right, float bottom, float left) => Num(top) + " " + Num(right) + " " + Num(bottom) + " " + Num(left); static string Hex(Color color) => "#" + ColorUtility.ToHtmlStringRGBA(color).ToLowerInvariant(); sealed class ThemeDump { [JsonProperty("unityVersion")] public string UnityVersion { get; set; } [JsonProperty("theme")] public string Theme { get; set; } [JsonProperty("controls")] public List Controls { get; } = new List(); } sealed class ControlDump { [JsonProperty("control")] public string Control { get; set; } [JsonProperty("root")] public PartDump Root { get; set; } } sealed class PartDump { [JsonProperty("name")] public string Name { get; set; } [JsonProperty("type")] public string Type { get; set; } [JsonProperty("classes")] public List Classes { get; set; } [JsonProperty("style")] public Dictionary Style { get; set; } [JsonProperty("children")] public List Children { get; } = new List(); } } }