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();
}
}
}