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