using System;
using System.Collections.Generic;
using Uigenx.Animation;
using Uigenx.Binding;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.UIElements;
namespace Uigenx.Effects
{
public enum UgxMaterialParamKind
{
Float = 0,
Color = 1,
Vector = 2,
}
/// One material parameter driven from a data source path.
public readonly struct UgxMaterialParam
{
public readonly string Name;
public readonly UgxMaterialParamKind Kind;
public readonly string Path;
public UgxMaterialParam(string name, UgxMaterialParamKind kind, string path)
{
Name = name;
Kind = kind;
Path = path;
}
}
///
/// One row of the compiled material table. There is no material reference here on purpose: the
/// stylesheet assigns it with `-unity-material`, because generated C# cannot hold an asset
/// reference without a Resources lookup. Constants belong in the `.mat` asset, so a row exists
/// only when at least one parameter is data driven.
///
public readonly struct UgxMaterialEntry
{
public readonly string ElementName;
public readonly UgxMaterialParam[] Params;
public UgxMaterialEntry(string elementName, params UgxMaterialParam[] parameters)
{
ElementName = elementName;
Params = parameters ?? Array.Empty();
}
}
///
/// Assigns a material to an element through the USS `-unity-material` property and drives its
/// parameters from tokens or live data.
///
///
/// Verified against Unity 6000.5: `IStyle.unityMaterial` is a `StyleMaterialDefinition` and
/// `IResolvedStyle.unityMaterial` is a `MaterialDefinition`. `MaterialDefinition` is a struct,
/// so the definition is kept here and pushed back into the style after every change. The
/// material affects the element and all of its children, which is why per element instancing
/// is opt in. UI Shader Graph materials are URP only; on any other pipeline the binder warns
/// once and leaves the element unstyled.
///
public sealed class UgxMaterialBinder : IDisposable
{
static bool _pipelineWarningShown;
readonly VisualElement _element;
readonly Material _instance;
readonly List _bound = new List(4);
MaterialDefinition _definition;
INotifyBindablePropertyChanged _source;
IUgxDataSource _dataSource;
bool _disposed;
// True between Adopt and the first geometry pass, while the material is not resolved yet.
bool _adopting;
///
/// Drives the material the element already got from USS `-unity-material`, instead of
/// taking one as an argument. This is the path generated code uses: a C# table cannot hold
/// an asset reference, but USS resolves one at import time, so the stylesheet assigns the
/// material and this adopts it to drive its parameters.
///
///
/// The resolved style is not available until the element is in a panel and has been laid
/// out, so adoption is deferred to the first geometry pass and queued parameter bindings
/// are applied then.
///
public static UgxMaterialBinder Adopt(VisualElement element)
=> new UgxMaterialBinder(element);
UgxMaterialBinder(VisualElement element)
{
_element = element ?? throw new ArgumentNullException(nameof(element));
_adopting = true;
_element.RegisterCallback(OnFirstGeometry);
}
void OnFirstGeometry(GeometryChangedEvent evt)
{
_element.UnregisterCallback(OnFirstGeometry);
if (_disposed) return;
_adopting = false;
var resolved = _element.resolvedStyle.unityMaterial;
if (resolved.material == null)
{
Debug.LogWarning("[ugx] no -unity-material resolved on '" + _element.name +
"', so its material parameters were not applied. Check the USS.");
return;
}
_definition = resolved;
PushBound(null);
Push();
}
public UgxMaterialBinder(VisualElement element, Material material, bool instancePerElement = false)
{
_element = element ?? throw new ArgumentNullException(nameof(element));
if (material == null) throw new ArgumentNullException(nameof(material));
if (!IsSupportedPipeline())
{
if (!_pipelineWarningShown)
{
_pipelineWarningShown = true;
Debug.LogWarning("[ugx] UI materials need the Universal Render Pipeline. '" + material.name +
"' was not applied; the element renders without it.");
}
return;
}
if (instancePerElement)
{
_instance = new Material(material) { name = material.name + " (ugx instance)" };
_instance.hideFlags = HideFlags.HideAndDontSave;
}
_definition = MaterialDefinition.FromMaterial(_instance != null ? _instance : material);
Push();
}
public bool IsApplied => _definition.material != null;
public Material Material => _definition.material;
public void SetFloat(string name, float value)
{
if (!IsApplied) return;
_definition.SetFloat(name, value);
Push();
}
public void SetColor(string name, Color value)
{
if (!IsApplied) return;
_definition.SetColor(name, value);
Push();
}
public void SetVector(string name, Vector4 value)
{
if (!IsApplied) return;
_definition.SetVector(name, value);
Push();
}
public void SetTexture(string name, Texture value)
{
if (!IsApplied) return;
_definition.SetTexture(name, value);
Push();
}
/// Drives one material parameter from a data source path, updating on every change.
public void Bind(string parameterName, string path, UgxMaterialParamKind kind, IUgxDataSource source)
{
if (string.IsNullOrEmpty(parameterName) || string.IsNullOrEmpty(path)) return;
// While adopting there is no material yet, so the binding is recorded and applied when
// the material resolves.
if (!IsApplied && !_adopting) return;
_bound.Add(new Bound(parameterName, path, kind));
_dataSource = source;
PushBound(path);
if (_source != null || !(source is INotifyBindablePropertyChanged notifier)) return;
_source = notifier;
_source.propertyChanged += OnSourceChanged;
}
public void Dispose()
{
if (_disposed) return;
_disposed = true;
if (_adopting)
{
_adopting = false;
_element.UnregisterCallback(OnFirstGeometry);
}
if (_source != null)
{
_source.propertyChanged -= OnSourceChanged;
_source = null;
}
_bound.Clear();
if (IsApplied) _element.style.unityMaterial = StyleKeyword.Null;
_definition = default;
if (_instance != null)
{
if (Application.isPlaying) UnityEngine.Object.Destroy(_instance);
else UnityEngine.Object.DestroyImmediate(_instance);
}
}
void OnSourceChanged(object sender, BindablePropertyChangedEventArgs args)
{
if (_disposed) return;
PushBound((string)args.propertyName);
}
void PushBound(string path)
{
if (_dataSource == null) return;
bool changed = false;
for (int i = 0; i < _bound.Count; i++)
{
var bound = _bound[i];
if (!string.IsNullOrEmpty(path) && bound.Path != path) continue;
if (!_dataSource.TryGet(bound.Path, out var raw)) continue;
switch (bound.Kind)
{
case UgxMaterialParamKind.Color:
if (TryColor(raw, out var color)) _definition.SetColor(bound.Name, color);
break;
case UgxMaterialParamKind.Vector:
if (raw is Vector4 v4) _definition.SetVector(bound.Name, v4);
else if (raw is Vector3 v3) _definition.SetVector(bound.Name, v3);
else if (raw is Vector2 v2) _definition.SetVector(bound.Name, v2);
break;
default:
_definition.SetFloat(bound.Name, ToFloat(raw));
break;
}
changed = true;
}
if (changed) Push();
}
void Push() => _element.style.unityMaterial = new StyleMaterialDefinition(_definition);
static bool TryColor(object raw, out Color color)
{
switch (raw)
{
case Color c:
color = c;
return true;
case Color32 c32:
color = c32;
return true;
case string s:
return UgxValueParser.TryColor(s, out color);
default:
color = default;
return false;
}
}
static float ToFloat(object raw)
{
switch (raw)
{
case float f: return f;
case double d: return (float)d;
case int i: return i;
case long l: return l;
case bool b: return b ? 1f : 0f;
case string s:
return UgxValueParser.TryFloat(s, out var parsed) ? parsed : 0f;
default: return 0f;
}
}
static bool IsSupportedPipeline()
{
var pipeline = GraphicsSettings.currentRenderPipeline;
if (pipeline == null) return false;
return pipeline.GetType().FullName?.Contains("Universal") == true;
}
readonly struct Bound
{
public readonly string Name;
public readonly string Path;
public readonly UgxMaterialParamKind Kind;
public Bound(string name, string path, UgxMaterialParamKind kind)
{
Name = name;
Path = path;
Kind = kind;
}
}
}
}