Files
2026-11-01 23:39:55 +02:00

1958 lines
74 KiB
C#

#if ENABLE_UIELEMENTS_MODULE && UNITY_ENABLE_CHECKS
#define ENABLE_RENDERING_DEBUGGER_UI
#endif
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
using Assert = UnityEngine.Assertions.Assert;
#if ENABLE_RENDERING_DEBUGGER_UI
using UnityEngine.UIElements;
#endif
namespace UnityEngine.Rendering
{
internal interface ISupportsLegacyStateHandling
{
bool RequiresLegacyStateHandling();
}
public partial class DebugUI
{
/// <summary>
/// Generic field.
/// </summary>
/// <example>
/// <code>
/// public class CustomRectField : DebugUI.Field&lt;Rect&gt;
/// {
/// protected override VisualElement Create()
/// {
/// var field = new RectField()
/// {
/// label = displayName,
/// };
/// return field;
/// }
/// }
/// </code>
/// </example>
/// <typeparam name="T">The type of data managed by the field.</typeparam>
public abstract class Field<T> : Widget
#pragma warning disable CS0618 // Type or member is obsolete
, IValueField
#pragma warning restore CS0618 // Type or member is obsolete
, ISupportsLegacyStateHandling
{
/// <summary>
/// Getter for this field.
/// </summary>
public Func<T> getter { get; set; }
/// <summary>
/// Setter for this field.
/// </summary>
public Action<T> setter { get; set; }
// This should be an `event` but they don't play nice with object initializers in the
// version of C# we use.
/// <summary>
/// Callback used when the value of the field changes.
/// </summary>
public Action<Field<T>, T> onValueChanged;
/// <summary>
/// Function used to validate the value when updating the field.
/// </summary>
/// <param name="value">Input value.</param>
/// <returns>Validated value.</returns>
object IValueField.ValidateValue(object value)
{
return ValidateValue((T)value);
}
/// <summary>
/// Function used to validate the value when updating the field.
/// </summary>
/// <param name="value">Input value.</param>
/// <returns>Validated value.</returns>
public virtual T ValidateValue(T value)
{
return value;
}
/// <summary>
/// Get the value of the field.
/// </summary>
/// <returns>Value of the field.</returns>
object IValueField.GetValue()
{
return GetValue();
}
/// <summary>
/// Get the value of the field.
/// </summary>
/// <returns>Value of the field.</returns>
public T GetValue()
{
Assert.IsNotNull(getter);
return getter();
}
/// <summary>
/// Set the value of the field.
/// </summary>
/// <param name="value">Input value.</param>
public void SetValue(object value)
{
SetValue((T)value);
}
/// <summary>
/// Set the value of the field.
/// </summary>
/// <param name="value">Input value.</param>
public virtual void SetValue(T value)
{
if (setter == null)
return;
var v = ValidateValue(value);
if (v == null || !v.Equals(getter()))
{
#if UNITY_EDITOR
T previousValue = GetValue();
onWidgetValueChangedAnalytic?.Invoke(queryPath, previousValue, v);
#endif
setter(v);
onValueChanged?.Invoke(this, v);
}
}
internal static Action<string, T, T> onWidgetValueChangedAnalytic;
// In order to support the legacy DebugState system, we are inspecting the closure of the `getter` lambda, to see if the captured
// data is using the ISerializedDebugDisplaySettings interface. We know that any data that uses the new interface does not need
// legacy state handling. This is a temporary solution until we fully migrate to the new system and remove DebugState.
bool ISupportsLegacyStateHandling.RequiresLegacyStateHandling()
{
bool FieldsHaveISerializedDebugDisplaySettings(object obj)
{
if (obj == null)
return false;
var fields = obj.GetType().GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
foreach (var field in fields)
{
var value = field.GetValue(obj);
if (value is ISerializedDebugDisplaySettings)
return true;
}
return false;
}
var getterClosure = getter.Target;
if (getterClosure != null)
{
bool foundISerializedDebugDisplaySettings = FieldsHaveISerializedDebugDisplaySettings(getterClosure);
return !foundISerializedDebugDisplaySettings;
}
return false;
}
}
/// <summary>
/// Boolean field.
/// </summary>
public class BoolField : Field<bool>
{
#if ENABLE_RENDERING_DEBUGGER_UI
/// <inheritdoc/>
protected override VisualElement Create()
{
var toggle = new UIElements.Toggle();
BaseFieldHelper.ConfigureBaseField(this, toggle);
return toggle;
}
#endif
}
/// <summary>
/// An array of checkboxes that Unity displays in a horizontal row.
/// </summary>
public class HistoryBoolField : BoolField
{
#if ENABLE_RENDERING_DEBUGGER_UI
/// <inheritdoc/>
protected override VisualElement Create()
{
var valueContainer = new UIElements.VisualElement();
valueContainer.AddToClassList("debug-window-historyboolfield");
var label = new Label(displayName)
{
style = { width = ValueTuple.GetLabelWidth(m_Context) }
};
label.AddToClassList("debug-window-search-filter-target");
valueContainer.Add(label);
var boolField = new DebugUI.BoolField()
{
displayName = string.Empty,
tooltip = tooltip,
getter = getter,
setter = setter,
};
childWidgets.Add(boolField);
valueContainer.Add(boolField.ToVisualElement(m_Context));
foreach (var value in historyGetter)
{
var historyBoolField = new DebugUI.BoolField()
{
displayName = string.Empty,
getter = value
};
childWidgets.Add(historyBoolField);
var field = historyBoolField.ToVisualElement(m_Context);
field.SetEnabled(false);
valueContainer.Add(field);
}
valueContainer.AddToClassList(UIElements.BaseField<bool>.alignedFieldUssClassName);
return valueContainer;
}
#endif
internal List<Widget> childWidgets { private set; get; } = new List<Widget>();
/// <summary>
/// History getter for this field.
/// </summary>
public Func<bool>[] historyGetter { get; set; }
/// <summary>
/// Depth of the field's history.
/// </summary>
public int historyDepth => historyGetter?.Length ?? 0;
/// <summary>
/// Get the value of the field at a certain history index.
/// </summary>
/// <param name="historyIndex">Index of the history to query.</param>
/// <returns>Value of the field at the provided history index.</returns>
public bool GetHistoryValue(int historyIndex)
{
Assert.IsNotNull(historyGetter);
Assert.IsTrue(historyIndex >= 0 && historyIndex < historyGetter.Length, "out of range historyIndex");
Assert.IsNotNull(historyGetter[historyIndex]);
return historyGetter[historyIndex]();
}
}
/// <summary>
/// A slider for an integer.
/// </summary>
public class IntField : Field<int>
{
/// <summary>
/// Minimum value function.
/// </summary>
public Func<int> min;
/// <summary>
/// Maximum value function.
/// </summary>
public Func<int> max;
// Runtime-only
/// <summary>
/// Step increment.
/// </summary>
public int incStep = 1;
/// <summary>
/// Step increment multiplier.
/// </summary>
[Obsolete("Use incStepMult instead #from(6000.5) (UnityUpgradable) -> incStepMult")]
public int intStepMult = 10;
/// <summary>
/// Step increment multiplier.
/// </summary>
public int incStepMult = 10;
/// <summary>
/// Function used to validate the value when updating the field.
/// </summary>
/// <param name="value">Input value.</param>
/// <returns>Validated value.</returns>
public override int ValidateValue(int value)
{
if (min != null) value = Mathf.Max(value, min());
if (max != null) value = Mathf.Min(value, max());
return value;
}
internal override void OnDecrement(bool fast)
{
int currentValue = GetValue();
int step = fast ? incStepMult : incStep;
int minValue = min != null ? min() : int.MinValue;
// Check if subtraction would cause overflow, set to max value instead of wrapping around
int newValue = currentValue >= minValue + step ? currentValue - step : minValue;
SetValue(newValue);
}
internal override void OnIncrement(bool fast)
{
int currentValue = GetValue();
int step = fast ? incStepMult : incStep;
int maxValue = max != null ? max() : int.MaxValue;
// Check if addition would cause overflow, set to max value instead of wrapping around
int newValue = currentValue <= maxValue-step ? currentValue + step : maxValue;
SetValue(newValue);
}
#if ENABLE_RENDERING_DEBUGGER_UI
/// <inheritdoc/>
protected override VisualElement Create()
{
if (m_Context.IsAnyRuntimeContext())
{
var field = new UIElements.IntegerField();
DebugUIStepperHelper.AddStepper(
field,
SetValue,
GetValue,
onDecrement: OnDecrement,
onIncrement: OnIncrement
);
BaseFieldHelper.ConfigureBaseField(this, field);
return field;
}
if (min != null || max != null)
{
var field = new UIElements.SliderInt(min?.Invoke() ?? int.MinValue, max?.Invoke() ?? int.MaxValue);
BaseFieldHelper.ConfigureBaseField(this, field);
field.showInputField = true;
return field;
}
else
{
var field = new UIElements.IntegerField();
BaseFieldHelper.ConfigureBaseField(this, field);
return field;
}
}
#endif
}
/// <summary>
/// A slider for a positive integer.
/// </summary>
public class UIntField : Field<uint>
{
/// <summary>
/// Minimum value function.
/// </summary>
public Func<uint> min;
/// <summary>
/// Maximum value function.
/// </summary>
public Func<uint> max;
// Runtime-only
/// <summary>
/// Step increment.
/// </summary>
public uint incStep = 1u;
/// <summary>
/// Step increment multiplier.
/// </summary>
[Obsolete("Use incStepMult instead #from(6000.5) (UnityUpgradable) -> incStepMult")]
public uint intStepMult = 10u;
/// <summary>
/// Step increment multiplier.
/// </summary>
public uint incStepMult = 10u;
/// <summary>
/// Function used to validate the value when updating the field.
/// </summary>
/// <param name="value">Input value.</param>
/// <returns>Validated value.</returns>
public override uint ValidateValue(uint value)
{
if (min != null) value = (uint)Mathf.Max(value, min());
if (max != null) value = (uint)Mathf.Min(value, max());
return value;
}
internal override void OnDecrement(bool fast)
{
uint currentValue = GetValue();
uint step = fast ? incStepMult : incStep;
uint minValue = min != null ? min() : uint.MinValue;
// Check if subtraction would cause overflow, set to max value instead of wrapping around
uint newValue = currentValue >= minValue + step ? currentValue - step : minValue;
SetValue(newValue);
}
internal override void OnIncrement(bool fast)
{
uint currentValue = GetValue();
uint step = fast ? incStepMult : incStep;
uint maxValue = max != null ? max() : uint.MaxValue;
// Check if addition would cause overflow, set to max value instead of wrapping around
uint newValue = currentValue <= maxValue-step ? currentValue + step : maxValue;
SetValue(newValue);
}
#if ENABLE_RENDERING_DEBUGGER_UI
/// <inheritdoc/>
protected override VisualElement Create()
{
var field = new UIElements.UnsignedIntegerField();
if (m_Context.IsAnyRuntimeContext())
{
field.RegisterCallback<FocusOutEvent>(evt =>
{
var validatedValue = ValidateValue(field.value);
if (validatedValue != field.value)
{
field.SetValueWithoutNotify(validatedValue);
SetValue(validatedValue);
}
});
DebugUIStepperHelper.AddStepper(
field,
SetValue,
GetValue,
onDecrement: OnDecrement,
onIncrement: OnIncrement
);
}
BaseFieldHelper.ConfigureBaseField(this, field);
field.RegisterCallback<ChangeEvent<uint>>((evt) =>
{
field.SetValueWithoutNotify(ValidateValue(evt.newValue));
});
return field;
}
#endif
}
/// <summary>
/// A slider for a float.
/// </summary>
public class FloatField : Field<float>
{
/// <summary>
/// Minimum value function.
/// </summary>
public Func<float> min;
/// <summary>
/// Maximum value function.
/// </summary>
public Func<float> max;
// Runtime-only
/// <summary>
/// Step increment.
/// </summary>
public float incStep = 0.1f;
/// <summary>
/// Step increment multiplier.
/// </summary>
public float incStepMult = 10f;
/// <summary>
/// Number of decimals.
/// </summary>
public int decimals = 3;
/// <summary>
/// Function used to validate the value when updating the field.
/// </summary>
/// <param name="value">Input value.</param>
/// <returns>Validated value.</returns>
public override float ValidateValue(float value)
{
if (min != null) value = Mathf.Max(value, min());
if (max != null) value = Mathf.Min(value, max());
return value;
}
#if ENABLE_RENDERING_DEBUGGER_UI
internal override void OnDecrement(bool fast)
{
float currentValue = GetValue();
float step = fast ? incStepMult : incStep;
float minValue = min != null ? min() : float.MinValue;
// Check if subtraction would cause overflow, set to max value instead of wrapping around
float newValue = currentValue >= minValue + step ? currentValue - step : minValue;
// Float precision: detect desired number of decimal places based on the step, and round to that
newValue = DebugUIStepperHelper.RoundToPrecision(newValue, currentValue, step);
SetValue(newValue);
}
internal override void OnIncrement(bool fast)
{
float currentValue = GetValue();
float step = fast ? incStepMult : incStep;
float maxValue = max != null ? max() : float.MaxValue;
// Check if addition would cause overflow, set to max value instead of wrapping around
float newValue = currentValue <= maxValue-step ? currentValue + step : maxValue;
// Float precision: detect desired number of decimal places based on the step, and round to that
newValue = DebugUIStepperHelper.RoundToPrecision(newValue, currentValue, step);
SetValue(newValue);
}
/// <inheritdoc/>
protected override VisualElement Create()
{
if (m_Context.IsAnyRuntimeContext())
{
var field = new UIElements.FloatField();
DebugUIStepperHelper.AddStepper(
field,
SetValue,
GetValue,
onDecrement: OnDecrement,
onIncrement: OnIncrement
);
BaseFieldHelper.ConfigureBaseField(this, field);
return field;
}
if (min != null || max != null)
{
var field = new UIElements.Slider(min?.Invoke() ?? float.MinValue, max?.Invoke() ?? float.MaxValue);
BaseFieldHelper.ConfigureBaseField(this, field);
field.showInputField = true;
return field;
}
else
{
var field = new UIElements.FloatField();
BaseFieldHelper.ConfigureBaseField(this, field);
return field;
}
}
#endif
}
/// <summary>
/// Field that displays <see cref="RenderingLayerMask"/>
/// </summary>
public class RenderingLayerField : Field<RenderingLayerMask>, IContainer
{
static readonly NameAndTooltip s_RenderingLayerColors = new()
{
name = "Layers Color",
tooltip = "Select the display color for each Rendering Layer"
};
private string[] m_RenderingLayersNames = Array.Empty<string>();
private int m_DefinedRenderingLayersCount = -1;
private int maxRenderingLayerCount
{
get
{
#if UNITY_EDITOR
if (UnityEditor.Rendering.EditorGraphicsSettings.
TryGetFirstRenderPipelineSettingsFromInterface<UnityEditor.Rendering.RenderingLayersLimitSettings>(out var settings))
return Mathf.Min(settings.maxSupportedRenderingLayers, RenderingLayerMask.GetRenderingLayerCount());
#endif
return RenderingLayerMask.GetRenderingLayerCount();
}
}
#if ENABLE_RENDERING_DEBUGGER_UI
protected override VisualElement Create()
{
var maskField = new UIElements.MaskField(displayName, new List<string>(m_RenderingLayersNames), 0);
maskField.labelElement.AddToClassList("debug-window-search-filter-target");
maskField.RegisterCallback<ChangeEvent<int>>(evt =>
{
SetValue(evt.newValue);
});
this.ScheduleTracked(maskField, () => maskField.schedule.Execute(() =>
{
var value = GetValue();
maskField.SetValueWithoutNotify(Convert.ToInt32(value));
})
.Every(100));
maskField.AddToClassList(UIElements.BaseField<int>.alignedFieldUssClassName);
HackPopupHoverColor(maskField, m_Context);
var content = new VisualElement();
content.AddToClassList("debug-window-renderinglayerfield__content");
foreach (var child in children)
{
var childUIElement = child.ToVisualElement(m_Context);
if (childUIElement != null)
{
childUIElement.RemoveFromClassList("debug-window-foldout");
content.Add(childUIElement);
}
}
VisualElement container = new VisualElement();
container.AddToClassList("unity-inspector-element");
container.AddToClassList("debug-window-renderinglayerfield");
container.Add(maskField);
container.Add(content);
return container;
}
#endif
private void Resize()
{
m_DefinedRenderingLayersCount = RenderingLayerMask.GetDefinedRenderingLayerCount();
// Fill layer names
m_RenderingLayersNames = new string[maxRenderingLayerCount];
for (int i = 0; i < maxRenderingLayerCount; i++)
{
var definedLayerName = RenderingLayerMask.RenderingLayerToName(i);
if (string.IsNullOrEmpty(definedLayerName))
definedLayerName = $"Unused Rendering Layer {i}";
m_RenderingLayersNames[i] = definedLayerName;
}
// Foldout + Color for each layer
m_RenderingLayersColors.Clear();
var layersColor = new DebugUI.Foldout()
{
nameAndTooltip = s_RenderingLayerColors,
flags = Flags.EditorOnly,
parent = this,
};
m_RenderingLayersColors.Add(layersColor);
for (int i = 0; i < m_RenderingLayersNames.Length; i++)
{
var index = i; // capture the variable for the color field index
layersColor.children.Add(new DebugUI.ColorField
{
displayName = m_RenderingLayersNames[index],
getter = () =>
{
Assert.IsNotNull(getRenderingLayerColor, "Please specify a method for getting the rendering layer color");
return getRenderingLayerColor(index);
},
setter = value =>
{
Assert.IsNotNull(setRenderingLayerColor, "Please specify a method for setting the rendering layer color");
setRenderingLayerColor(value, index);
}
});
}
GenerateQueryPath();
}
/// <summary>
/// Obtains the list of the available rendering layer names
/// </summary>
public string[] renderingLayersNames
{
get
{
if (m_DefinedRenderingLayersCount != RenderingLayerMask.GetDefinedRenderingLayerCount())
{
Resize();
}
return m_RenderingLayersNames;
}
}
private ObservableList<Widget> m_RenderingLayersColors = new ObservableList<Widget>();
/// <summary>
/// Gets the list of widgets representing the rendering layer colors.
/// </summary>
public ObservableList<Widget> children
{
get
{
if (m_DefinedRenderingLayersCount != RenderingLayerMask.GetDefinedRenderingLayerCount())
{
Resize();
}
return m_RenderingLayersColors;
}
}
/// <summary>
/// Obtains the color in a given index
/// </summary>
public Func<int, Vector4> getRenderingLayerColor { get; set; }
/// <summary>
/// Sets the color for a given index
/// </summary>
public Action<Vector4, int> setRenderingLayerColor { get; set; }
internal override void GenerateQueryPath()
{
base.GenerateQueryPath();
int numChildren = children.Count;
for (int i = 0; i < numChildren; i++)
children[i].GenerateQueryPath();
}
}
/// <summary>
/// Generic <see cref="EnumField"/> that stores enumNames and enumValues
/// </summary>
/// <typeparam name="T">The inner type of the field</typeparam>
public abstract class EnumField<T> : Field<T>
{
#if ENABLE_RENDERING_DEBUGGER_UI
/// <inheritdoc/>
protected override VisualElement Create()
{
var field = new UIElements.PopupField<string>()
{
label = displayName,
choices = enumNames.Select(e => e.text).ToList()
};
field.AddToClassList("debug-window-enumfield");
field.labelElement.AddToClassList("debug-window-search-filter-target");
field.AddToClassList(UIElements.BaseField<int>.alignedFieldUssClassName);
this.ScheduleTracked(field, () => field.schedule.Execute(() =>
{
T value = GetValue();
var index = Array.IndexOf(enumValues, value);
if (index >= 0 && index < enumNames.Length)
{
var expectedValue = enumNames[index].text;
if (field.value != expectedValue)
{
field.SetValueWithoutNotify(expectedValue);
}
}
}).Every(100));
m_AdditionalSearchText = string.Join(",", field.choices);
return field;
}
#endif
/// <summary>
/// List of names of the enumerator entries.
/// </summary>
public GUIContent[] enumNames;
private int[] m_EnumValues;
/// <summary>
/// List of values of the enumerator entries.
/// </summary>
public int[] enumValues
{
get => m_EnumValues;
set
{
if (value?.Distinct().Count() != value?.Count())
Debug.LogWarning($"{displayName} - The values of the enum are duplicated, this might lead to a errors displaying the enum");
m_EnumValues = value;
}
}
// Space-delimit PascalCase (https://stackoverflow.com/questions/155303/net-how-can-you-split-a-caps-delimited-string-into-an-array)
static readonly Regex s_NicifyRegEx = new("([a-z](?=[A-Z])|[A-Z](?=[A-Z][a-z]))", RegexOptions.Compiled);
/// <summary>
/// Automatically fills the enum names with a given <see cref="Type"/>
/// </summary>
/// <param name="enumType">The enum type</param>
/// <param name="removeZeroElement">Whether the item with the value zero should be removed from enumNames and enumValues</param>
protected void AutoFillFromType(Type enumType, bool removeZeroElement = false)
{
if (enumType == null || !enumType.IsEnum)
throw new ArgumentException($"{nameof(enumType)} must not be null and it must be an Enum type");
using (ListPool<GUIContent>.Get(out var tmpNames))
using (ListPool<int>.Get(out var tmpValues))
{
var enumEntries = enumType.GetFields(BindingFlags.Public | BindingFlags.Static)
.Where(fieldInfo => !fieldInfo.IsDefined(typeof(ObsoleteAttribute)) && !fieldInfo.IsDefined(typeof(HideInInspector)));
foreach (var fieldInfo in enumEntries)
{
var description = fieldInfo.GetCustomAttribute<InspectorNameAttribute>();
var displayName = new GUIContent(description == null ? s_NicifyRegEx.Replace(fieldInfo.Name, "$1 ") : description.displayName);
int fieldValue = (int)Enum.Parse(enumType, fieldInfo.Name);
if (removeZeroElement && fieldValue == 0)
continue;
tmpNames.Add(displayName);
tmpValues.Add(fieldValue);
}
enumNames = tmpNames.ToArray();
enumValues = tmpValues.ToArray();
}
}
}
#if ENABLE_RENDERING_DEBUGGER_UI
private static void HackPopupHoverColor(VisualElement popupField, in DebugUI.Context context)
{
if (context == DebugUI.Context.Runtime)
{
// For some reason. it seems impossible to override the hover color of the popup field.
// This works because C# style overrides have higher precedence than any USS stuff.
Color hoverColor = new Color32(0x66, 0x66, 0x66, 0xFF); // Should match --widget-background-color-hover
popupField.RegisterCallback<MouseEnterEvent>(evt =>
{
var inputElement = popupField.Q<VisualElement>(className: "unity-base-field__input");
if (inputElement != null)
inputElement.style.backgroundColor = hoverColor;
});
popupField.RegisterCallback<MouseLeaveEvent>(evt =>
{
var inputElement = popupField.Q<VisualElement>(className: "unity-base-field__input");
if (inputElement != null)
inputElement.style.backgroundColor = new StyleColor(StyleKeyword.Null);
});
}
}
#endif
/// <summary>
/// A dropdown that contains the values from an enum.
/// </summary>
public class EnumField : EnumField<int>
{
#if ENABLE_RENDERING_DEBUGGER_UI
/// <inheritdoc/>
protected override VisualElement Create()
{
var field = new UIElements.PopupField<string>()
{
label = displayName,
choices = enumNames.Select(e => e.text).ToList()
};
field.AddToClassList("debug-window-enumfield");
field.labelElement.AddToClassList("debug-window-search-filter-target");
field.AddToClassList(UIElements.BaseField<int>.alignedFieldUssClassName);
HackPopupHoverColor(field, m_Context);
field.RegisterCallback<ChangeEvent<string>>(evt =>
{
for (int i = 0; i < enumNames.Length; i++)
{
if (evt.newValue == enumNames[i].text)
{
SetValue(enumValues[i]);
break;
}
}
});
this.ScheduleTracked(field, () => field.schedule.Execute(() =>
{
// https://jira.unity3d.com/browse/UUM-138138: Clamp currentIndex to 0. This matches old IMGUI
// DebugUIDrawer behavior where a negative index is sometimes used to denote an invalid/none value.
int index = Mathf.Max(0, currentIndex);
if (index < enumNames.Length)
field.SetValueWithoutNotify(enumNames[index].text);
}).Every(100));
m_AdditionalSearchText = string.Join(",", field.choices);
return field;
}
#endif
internal int[] quickSeparators;
private int[] m_Indexes;
internal int[] indexes => m_Indexes ??= Enumerable.Range(0, enumNames?.Length ?? 0).ToArray();
/// <summary>
/// Get the enumeration value index.
/// </summary>
public Func<int> getIndex { get; set; }
/// <summary>
/// Set the enumeration value index.
/// </summary>
public Action<int> setIndex { get; set; }
/// <summary>
/// Current enumeration value index.
/// </summary>
public int currentIndex
{
get => getIndex();
set => setIndex(value);
}
private Type m_Type;
/// <summary>
/// Generates enumerator values and names automatically based on the provided type.
/// </summary>
public Type autoEnum
{
set
{
if (m_Type != value)
{
AutoFillFromType(value);
InitQuickSeparators();
m_Type = value;
}
}
}
internal void InitQuickSeparators()
{
var enumNamesPrefix = enumNames.Select(x =>
{
string[] splitted = x.text.Split('/');
if (splitted.Length == 1)
return "";
else
return splitted[0];
});
quickSeparators = new int[enumNamesPrefix.Distinct().Count()];
string lastPrefix = null;
for (int i = 0, wholeNameIndex = 0; i < quickSeparators.Length; ++i)
{
var currentTestedPrefix = enumNamesPrefix.ElementAt(wholeNameIndex);
while (lastPrefix == currentTestedPrefix)
{
currentTestedPrefix = enumNamesPrefix.ElementAt(++wholeNameIndex);
}
lastPrefix = currentTestedPrefix;
quickSeparators[i] = wholeNameIndex++;
}
}
/// <summary>
/// Set the value of the field.
/// </summary>
/// <param name="value">Input value.</param>
public override void SetValue(int value)
{
Assert.IsNotNull(setter);
var validValue = ValidateValue(value);
// There might be cases that the value does not map the index, look for the correct index
var newCurrentIndex = Array.IndexOf(enumValues, validValue);
if (currentIndex != newCurrentIndex && !validValue.Equals(getter()))
{
#if UNITY_EDITOR
int previousValue = GetValue();
onWidgetValueChangedAnalytic?.Invoke(queryPath, previousValue, validValue);
#endif
setter(validValue);
onValueChanged?.Invoke(this, validValue);
if (newCurrentIndex > -1)
currentIndex = newCurrentIndex;
}
}
}
/// <summary>
/// A dropdown that contains a list of Unity objects.
/// </summary>
public class ObjectPopupField : Field<Object>
{
#if ENABLE_RENDERING_DEBUGGER_UI
/// <inheritdoc/>
protected override VisualElement Create()
{
var choices = new List<UnityEngine.Object>() { null };
choices.AddRange(getObjects());
var field = new UIElements.PopupField<UnityEngine.Object>()
{
label = displayName,
choices = choices,
formatListItemCallback = o => o != null ? o.name : "None",
formatSelectedValueCallback = o => o != null ? o.name : "None"
};
field.AddToClassList("debug-window-objectpopupfield");
field.labelElement.AddToClassList("debug-window-search-filter-target");
HackPopupHoverColor(field, m_Context);
field.RegisterCallback<ChangeEvent<UnityEngine.Object>>(evt =>
{
SetValue(evt.newValue);
});
this.ScheduleTracked(field, () => field.schedule.Execute(() =>
{
field.SetValueWithoutNotify(GetValue());
}).Every(100));
field.AddToClassList(UIElements.BaseField<UnityEngine.Object>.alignedFieldUssClassName);
return field;
}
#endif
/// <summary>
/// Callback to obtain the elemtents of the pop up
/// </summary>
public Func<IEnumerable<Object>> getObjects { get; set; }
}
/// <summary>
/// A dropdown that contains a list of cameras
/// </summary>
public class CameraSelector : ObjectPopupField
{
/// <summary>
/// A dropdown that contains a list of cameras
/// </summary>
public CameraSelector()
{
displayName = "Camera";
getObjects = () => cameras;
}
private Camera[] m_CamerasArray;
private List<Camera> m_Cameras = new List<Camera>();
#if ENABLE_RENDERING_DEBUGGER_UI
/// <inheritdoc/>
protected override VisualElement Create()
{
var objectPopUpField = base.Create() as UIElements.PopupField<UnityEngine.Object>;
if (objectPopUpField == null)
return new Label("Error creating CameraSelector field");
objectPopUpField.choices ??= new List<UnityEngine.Object>() { null };
// Refresh the dropdown choices to keep it in sync with available cameras in the scene.
// NOTE: If the currently selected camera is deleted, PopupField handles it internally,
// so we only need to maintain the available choices list.
this.ScheduleTracked(objectPopUpField, () => objectPopUpField.schedule.Execute(() =>
{
// Using ListPool and SequenceEqual to avoid unnecessary allocations and UI updates
using (UnityEngine.Pool.ListPool<UnityEngine.Object>.Get(out var tmp))
{
tmp.Add(null);
tmp.AddRange(getObjects());
if (!tmp.SequenceEqual(objectPopUpField.choices))
{
objectPopUpField.choices.Clear();
objectPopUpField.choices.AddRange(tmp);
}
}
}).Every(500));
return objectPopUpField;
}
#endif
IEnumerable<Camera> cameras
{
get
{
m_Cameras.Clear();
#if UNITY_EDITOR
if (UnityEditor.SceneView.lastActiveSceneView != null)
{
var sceneCamera = UnityEditor.SceneView.lastActiveSceneView.camera;
if (sceneCamera != null)
m_Cameras.Add(sceneCamera);
}
#endif
if (m_CamerasArray == null || m_CamerasArray.Length != Camera.allCamerasCount)
{
m_CamerasArray = new Camera[Camera.allCamerasCount];
}
Camera.GetAllCameras(m_CamerasArray);
foreach (var camera in m_CamerasArray)
{
if (camera == null)
continue;
if (camera.cameraType != CameraType.Preview && camera.cameraType != CameraType.Reflection)
{
if (camera.TryGetComponent<IAdditionalData>(out _))
m_Cameras.Add(camera);
}
}
return m_Cameras;
}
}
}
/// <summary>
/// Enumerator field with history.
/// </summary>
public class HistoryEnumField : EnumField
{
#if ENABLE_RENDERING_DEBUGGER_UI
/// <inheritdoc/>
protected override VisualElement Create()
{
var valueContainer = new UIElements.VisualElement();
valueContainer.AddToClassList("debug-window-historyenum");
var label = new Label(displayName)
{
style = { width = ValueTuple.GetLabelWidth(m_Context) }
};
label.AddToClassList("debug-window-search-filter-target");
valueContainer.Add(label);
var enumField = new DebugUI.EnumField()
{
displayName = string.Empty,
tooltip = tooltip,
getter = getter,
setter = setter,
enumNames = enumNames,
enumValues = enumValues,
getIndex = getIndex,
setIndex = setIndex,
};
childWidgets.Add(enumField);
valueContainer.Add(enumField.ToVisualElement(m_Context));
foreach (var value in historyIndexGetter)
{
var historyEnumField = new DebugUI.EnumField()
{
displayName = string.Empty,
enumNames = enumNames,
enumValues = enumValues,
getIndex = value,
setIndex = i => {},
getter = value
};
childWidgets.Add(historyEnumField);
var field = historyEnumField.ToVisualElement(m_Context);
field.SetEnabled(false);
valueContainer.Add(field);
}
var enumNameStrings = enumNames.Select(e => e.text).ToList();
m_AdditionalSearchText = string.Join(",", enumNameStrings);
return valueContainer;
}
#endif
internal List<Widget> childWidgets { private set; get; } = new List<Widget>();
/// <summary>
/// History getter for this field.
/// </summary>
public Func<int>[] historyIndexGetter { get; set; }
/// <summary>
/// Depth of the field's history.
/// </summary>
public int historyDepth => historyIndexGetter?.Length ?? 0;
/// <summary>
/// Get the value of the field at a certain history index.
/// </summary>
/// <param name="historyIndex">Index of the history to query.</param>
/// <returns>Value of the field at the provided history index.</returns>
public int GetHistoryValue(int historyIndex)
{
Assert.IsNotNull(historyIndexGetter);
Assert.IsTrue(historyIndex >= 0 && historyIndex < historyIndexGetter.Length, "out of range historyIndex");
Assert.IsNotNull(historyIndexGetter[historyIndex]);
return historyIndexGetter[historyIndex]();
}
}
/// <summary>
/// Bitfield enumeration field.
/// </summary>
public class BitField : EnumField<Enum>
{
#if ENABLE_RENDERING_DEBUGGER_UI
/// <inheritdoc/>
protected override VisualElement Create()
{
var enumNameStrings = enumNames.Select(e => e.text).ToList();
var maskField = new UIElements.MaskField(displayName, enumNameStrings, 0);
maskField.AddToClassList("debug-window-bitfield");
maskField.labelElement.AddToClassList("debug-window-search-filter-target");
HackPopupHoverColor(maskField, m_Context);
maskField.RegisterCallback<ChangeEvent<int>>(evt =>
{
var value = Enum.Parse(m_EnumType, evt.newValue.ToString()) as Enum;
SetValue(value);
});
this.ScheduleTracked(maskField, () => maskField.schedule.Execute(() =>
{
var value = GetValue();
maskField.SetValueWithoutNotify(Convert.ToInt32(value));
})
.Every(100));
maskField.AddToClassList(UIElements.BaseField<int>.alignedFieldUssClassName);
m_AdditionalSearchText = string.Join(",", enumNameStrings);
return maskField;
}
#endif
Type m_EnumType;
/// <summary>
/// Generates bitfield values and names automatically based on the provided type.
/// </summary>
public Type enumType
{
get => m_EnumType;
set
{
if (!value.IsEnum ||
value.GetCustomAttributes(typeof(FlagsAttribute), inherit: false).Length == 0)
{
throw new ArgumentException($"{nameof(value)} must be an Enum type with the Flags attribute");
}
m_EnumType = value;
// Automatically fill enum names and values. If there is a zero value, remove it because a
// "Nothing" value will get automatically added by the MaskField control.
AutoFillFromType(value, removeZeroElement: true);
}
}
}
#if ENABLE_RENDERING_DEBUGGER_UI
public class RuntimeColorField : UIElements.BaseField<Color>
{
private VisualElement colorPreview;
private UIElements.FloatField redField;
private UIElements.FloatField greenField;
private UIElements.FloatField blueField;
public RuntimeColorField(string label, bool showRGB) : base(label, null)
{
CreateVisualElements(showRGB);
}
private void CreateVisualElements(bool showRGB)
{
contentContainer.AddToClassList("debug-window-colorfield-runtime-container");
contentContainer.style.flexDirection = FlexDirection.Row;
// RGB fields
if (showRGB )
{
redField = CreateColorComponentField();
greenField = CreateColorComponentField();
blueField = CreateColorComponentField();
contentContainer.Add(redField);
contentContainer.Add(greenField);
contentContainer.Add(blueField);
}
// Color preview
colorPreview = new VisualElement();
colorPreview.AddToClassList("debug-window-colorfield-preview");
colorPreview.AddToClassList("unity-base-field");
colorPreview.style.flexBasis = new Length(50, LengthUnit.Percent);
colorPreview.style.flexGrow = 1;
colorPreview.style.flexShrink = 0;
contentContainer.Add(colorPreview);
}
private UIElements.FloatField CreateColorComponentField()
{
var field = new UIElements.FloatField();
field.isReadOnly = true;
field.focusable = false;
field.style.flexGrow = 0;
field.style.flexShrink = 0;
field.formatString = "F2";
field.label = string.Empty;
return field;
}
public override void SetValueWithoutNotify(Color newValue)
{
base.SetValueWithoutNotify(newValue);
// Update color preview
colorPreview.style.backgroundColor = newValue;
// Update RGB fields to display current values
redField?.SetValueWithoutNotify(newValue.r);
greenField?.SetValueWithoutNotify(newValue.g);
blueField?.SetValueWithoutNotify(newValue.b);
}
}
#endif
/// <summary>
/// Color field.
/// </summary>
public class ColorField : Field<Color>
{
#if ENABLE_RENDERING_DEBUGGER_UI
/// <inheritdoc/>
protected override VisualElement Create()
{
UIElements.BaseField<Color> field = null;
#if UNITY_EDITOR
if (m_Context == DebugUI.Context.Editor && showPicker)
{
field = new UnityEditor.UIElements.ColorField()
{
label = displayName,
};
}
#endif
if (field == null)
{
field = new RuntimeColorField(displayName, showPicker);
}
if (field != null)
{
field.AddToClassList("debug-window-colorfield");
field.labelElement.AddToClassList("debug-window-search-filter-target");
field.AddToClassList(UIElements.BaseField<Color>.alignedFieldUssClassName);
field.RegisterCallback<ChangeEvent<Color>>(evt => SetValue(evt.newValue));
this.ScheduleTracked(field, () => field.schedule.Execute(() =>
{
field.SetValueWithoutNotify((Color)Convert.ChangeType(GetValue(), typeof(Color)));
}).Every(100));
}
return field;
}
#endif
/// <summary>
/// HDR color.
/// </summary>
public bool hdr = false;
/// <summary>
/// Show alpha of the color field.
/// </summary>
public bool showAlpha = true;
// Editor-only
/// <summary>
/// Show the color picker.
/// </summary>
public bool showPicker = true;
// Runtime-only
/// <summary>
/// Step increment.
/// </summary>
public float incStep = 0.025f;
/// <summary>
/// Step increment multiplier.
/// </summary>
public float incStepMult = 5f;
/// <summary>
/// Number of decimals.
/// </summary>
public int decimals = 3;
/// <summary>
/// Function used to validate the value when updating the field.
/// </summary>
/// <param name="value">Input value.</param>
/// <returns>Validated value.</returns>
public override Color ValidateValue(Color value)
{
if (!hdr)
{
value.r = Mathf.Clamp01(value.r);
value.g = Mathf.Clamp01(value.g);
value.b = Mathf.Clamp01(value.b);
value.a = Mathf.Clamp01(value.a);
}
return value;
}
}
#if ENABLE_RENDERING_DEBUGGER_UI
class RuntimeBaseFieldWrapper<T> : UIElements.BaseField<T> where T : struct
{
public RuntimeBaseFieldWrapper(VisualElement container, string label) : base(label, container)
{
}
}
#endif
/// <summary>
/// Generic base class for vector fields with stepper support for Runtime.
/// </summary>
/// <typeparam name="T">The numeric vector type</typeparam>
public abstract class VectorField<T> : DebugUI.Field<T> where T : struct
{
// Runtime-only
/// <summary>
/// Step increment.
/// </summary>
public float incStep = 0.025f;
/// <summary>
/// Step increment multiplier.
/// </summary>
public float incStepMult = 10f;
/// <summary>
/// Number of decimals.
/// </summary>
public int decimals = 3;
#if ENABLE_RENDERING_DEBUGGER_UI
/// <summary>
/// Selected component index.
/// </summary>
internal int selectedComponent = -1;
/// <inheritdoc/>
protected override VisualElement Create()
{
UIElements.BaseField<T> vectorField;
if (m_Context.IsAnyRuntimeContext())
{
vectorField = CreateRuntimeVectorField();
}
else
{
vectorField = CreateEditorVectorField();
}
BaseFieldHelper.ConfigureBaseField(this, vectorField);
return vectorField;
}
private UIElements.BaseField<T> CreateRuntimeVectorField()
{
var fieldsContainer = new VisualElement();
fieldsContainer.AddToClassList($"debug-window-{GetVectorTypeName()}-runtime");
fieldsContainer.style.flexDirection = FlexDirection.Column;
fieldsContainer.delegatesFocus = true;
// Create component fields based on vector type
var componentFields = CreateComponentFields();
foreach (var field in componentFields)
{
fieldsContainer.Add(field);
}
// Sync values from data model to UI
this.ScheduleTracked(fieldsContainer, () => fieldsContainer.schedule.Execute(() =>
{
var value = GetValue();
var components = GetVectorComponents(value);
for (int i = 0; i < componentFields.Length && i < components.Length; i++)
{
componentFields[i].SetValueWithoutNotify(components[i]);
}
}).Every(100));
return new RuntimeBaseFieldWrapper<T>(fieldsContainer, displayName);
}
private UIElements.FloatField[] CreateComponentFields()
{
var componentCount = GetComponentCount();
var componentFields = new UIElements.FloatField[componentCount];
var componentNames = GetComponentNames();
for (int i = 0; i < componentCount; i++)
{
var componentIndex = i; // Capture for closure
var field = new UIElements.FloatField { formatString = $"F{decimals}" };
DebugUIStepperHelper.AddStepper(
field,
(newValue) => UpdateVectorComponent(componentIndex, newValue),
() => this[componentIndex],
onDecrement: (fast) => { selectedComponent = componentIndex; OnDecrement(fast); },
onIncrement: (fast) => { selectedComponent = componentIndex; OnIncrement(fast); }
);
field.RegisterCallback<FocusInEvent>(evt =>
{
selectedComponent = componentIndex;
});
// Add tooltip for component
field.focusable = true;
field.label = componentNames[i];
field.tooltip = $"{displayName} {componentNames[i]}";
componentFields[i] = field;
}
return componentFields;
}
private void UpdateVectorComponent(int componentIndex, float newValue)
{
var currentValue = GetValue();
var newVectorValue = SetVectorComponent(currentValue, componentIndex, newValue);
SetValue(newVectorValue);
}
internal override void OnDecrement(bool fast)
{
if (selectedComponent >= 0 && selectedComponent < GetComponentCount())
{
float currentValue = this[selectedComponent];
float step = fast ? incStepMult : incStep;
float minValue = float.MinValue;
// Check if subtraction would cause overflow, set to max value instead of wrapping around
float newValue = currentValue >= minValue + step ? currentValue - step : minValue;
// Float precision: detect desired number of decimal places based on the step, and round to that
newValue = DebugUIStepperHelper.RoundToPrecision(newValue, currentValue, step);
UpdateVectorComponent(selectedComponent, newValue);
}
}
internal override void OnIncrement(bool fast)
{
if (selectedComponent >= 0 && selectedComponent < GetComponentCount())
{
float currentValue = this[selectedComponent];
var step = fast ? incStepMult : incStep;
float maxValue = float.MaxValue;
// Check if subtraction would cause overflow, set to max value instead of wrapping around
float newValue = currentValue <= maxValue - step ? currentValue + step : maxValue;
// Float precision: detect desired number of decimal places based on the step, and round to that
newValue = DebugUIStepperHelper.RoundToPrecision(newValue, currentValue, step);
UpdateVectorComponent(selectedComponent, newValue);
}
}
// Abstract methods to be implemented by specific vector types
protected abstract float this[int index] { get; }
protected abstract int GetComponentCount();
protected abstract string[] GetComponentNames();
protected abstract string GetVectorTypeName();
protected abstract float[] GetVectorComponents(T vector);
protected abstract T SetVectorComponent(T vector, int componentIndex, float value);
protected abstract UIElements.BaseField<T> CreateEditorVectorField();
#endif
}
/// <summary>
/// Vector2 field.
/// </summary>
public class Vector2Field : VectorField<Vector2>
{
#if ENABLE_RENDERING_DEBUGGER_UI
protected override int GetComponentCount() => 2;
protected override string[] GetComponentNames() => new[] { "X", "Y" };
protected override string GetVectorTypeName() => "vector2field";
protected override float[] GetVectorComponents(Vector2 vector) =>
new[] { vector.x, vector.y };
protected override float this[int index]
{
get
{
var vector = GetValue();
return index switch
{
0 => vector.x,
1 => vector.y,
_ => 0
};
}
}
protected override Vector2 SetVectorComponent(Vector2 vector, int componentIndex, float value)
{
return componentIndex switch
{
0 => new Vector2(value, vector.y),
1 => new Vector2(vector.x, value),
_ => vector
};
}
protected override UIElements.BaseField<Vector2> CreateEditorVectorField() =>
new UIElements.Vector2Field();
#endif
}
/// <summary>
/// Vector3 field.
/// </summary>
public class Vector3Field : VectorField<Vector3>
{
#if ENABLE_RENDERING_DEBUGGER_UI
protected override int GetComponentCount() => 3;
protected override string[] GetComponentNames() => new[] { "X", "Y", "Z" };
protected override string GetVectorTypeName() => "vector3field";
protected override float[] GetVectorComponents(Vector3 vector) =>
new[] { vector.x, vector.y, vector.z };
protected override float this[int index]
{
get
{
var vector = GetValue();
return index switch
{
0 => vector.x,
1 => vector.y,
2 => vector.z,
_ => 0
};
}
}
protected override Vector3 SetVectorComponent(Vector3 vector, int componentIndex, float value)
{
return componentIndex switch
{
0 => new Vector3(value, vector.y, vector.z),
1 => new Vector3(vector.x, value, vector.z),
2 => new Vector3(vector.x, vector.y, value),
_ => vector
};
}
protected override UIElements.BaseField<Vector3> CreateEditorVectorField() =>
new UIElements.Vector3Field();
#endif
}
/// <summary>
/// Vector4 field.
/// </summary>
public class Vector4Field : VectorField<Vector4>
{
#if ENABLE_RENDERING_DEBUGGER_UI
protected override int GetComponentCount() => 4;
protected override string[] GetComponentNames() => new[] { "X", "Y", "Z", "W" };
protected override string GetVectorTypeName() => "vector4field";
protected override float[] GetVectorComponents(Vector4 vector) =>
new[] { vector.x, vector.y, vector.z, vector.w };
protected override float this[int index]
{
get
{
var vector = GetValue();
return index switch
{
0 => vector.x,
1 => vector.y,
2 => vector.z,
3 => vector.w,
_ => 0
};
}
}
protected override Vector4 SetVectorComponent(Vector4 vector, int componentIndex, float value)
{
return componentIndex switch
{
0 => new Vector4(value, vector.y, vector.z, vector.w),
1 => new Vector4(vector.x, value, vector.z, vector.w),
2 => new Vector4(vector.x, vector.y, value, vector.w),
3 => new Vector4(vector.x, vector.y, vector.z, value),
_ => vector
};
}
protected override UIElements.BaseField<Vector4> CreateEditorVectorField() =>
new UIElements.Vector4Field();
#endif
}
#if ENABLE_RENDERING_DEBUGGER_UI
private class RuntimeObjectField : UIElements.BaseField<Object>
{
private UIElements.Label objectPreview;
public RuntimeObjectField(string label, UIElements.Label visual) : base(label, visual)
{
objectPreview = visual;
}
public override void SetValueWithoutNotify(Object newValue)
{
base.SetValueWithoutNotify(newValue);
objectPreview.text = newValue!=null ? newValue.name : "None";
}
}
#endif
/// <summary>
/// A field for selecting a Unity object.
/// </summary>
public class ObjectField : Field<Object>
{
#if ENABLE_RENDERING_DEBUGGER_UI
/// <inheritdoc/>
protected override VisualElement Create()
{
UIElements.BaseField<UnityEngine.Object> field = null;
#if UNITY_EDITOR
if (m_Context == DebugUI.Context.Editor)
{
field = new UnityEditor.UIElements.ObjectField()
{
label = displayName
};
}
#endif
if (field == null)
{
var objectPreview = new UIElements.Label();
field = new RuntimeObjectField(displayName, objectPreview);
}
field.AddToClassList("debug-window-objectfield");
field.labelElement.AddToClassList("debug-window-search-filter-target");
field.AddToClassList(UIElements.BaseField<Object>.alignedFieldUssClassName);
field.RegisterCallback<ChangeEvent<Object>>(evt => SetValue(evt.newValue));
this.ScheduleTracked(field, () => field.schedule.Execute(() =>
{
var selectedObject = GetValue();
field.SetValueWithoutNotify(selectedObject);
}).Every(100));
return field;
}
#endif
/// <summary>
/// Object type.
/// </summary>
public Type type = typeof(Object);
}
/// <summary>
/// A list of fields for selecting Unity objects.
/// </summary>
public class ObjectListField : Field<Object[]>
{
#if ENABLE_RENDERING_DEBUGGER_UI
/// <inheritdoc/>
protected override VisualElement Create()
{
// Capture the context locally: m_Context is mutated each time ToVisualElement is called,
// so if the same widget is rendered into multiple panels (e.g. editor window + runtime overlay),
// it would otherwise reflect whichever panel was created last by the time Sync ticks.
var context = m_Context;
var container = new UIElements.Foldout { text = displayName };
container.AddToClassList("debug-window-objectlistfield");
container.Q(className: "unity-foldout__text")?.AddToClassList("debug-window-search-filter-target");
// One-way data -> UI sync.
//
// The widget is read-only by design: it lives inside the Volume debug Table which is
// marked isReadOnly = true, cascading a disabled-state to every cell. There is no
// UI -> data path (no ChangeEvent handler on inner fields, no setter on the outer field).
//
// On each 100ms tick: re-reads GetValue(), updates existing child fields in place via
// SetValueWithoutNotify, and only allocates / removes children when the non-null count changes.
void SyncToUI()
{
var values = GetValue();
int count = values?.Length ?? 0;
// When the source pads with trailing nulls (e.g. pool-rented interpolated VolumeParameters),
// hide them. Interior nulls are still rendered as disabled slots.
if (trimTrailingNulls)
{
while (count > 0 && values[count - 1] == null)
count--;
}
for (int i = 0; i < count; i++)
{
var child = values[i];
BaseField<Object> field;
if (i < container.childCount)
{
field = (BaseField<Object>)container[i];
field.SetValueWithoutNotify(child);
}
else
{
field = null;
#if UNITY_EDITOR
if (context == Context.Editor)
field = new UnityEditor.UIElements.ObjectField { objectType = child != null ? child.GetType() : typeof(Object) };
#endif
field ??= new RuntimeObjectField(string.Empty, new Label());
field.AddToClassList("debug-window-objectfield");
container.Add(field);
// Defer first set so the ObjectField's picker UI initializes first.
var capturedChild = child;
field.schedule.Execute(() => field.SetValueWithoutNotify(capturedChild));
}
// Null entries render as disabled slots so the displayed shape matches the source array.
field.SetEnabled(child != null);
}
while (container.childCount > count)
container.RemoveAt(container.childCount - 1);
}
SyncToUI();
this.ScheduleTracked(container, context, () => container.schedule.Execute(SyncToUI).Every(100));
return container;
}
#endif
/// <summary>
/// Objects type.
/// </summary>
public Type type = typeof(Object);
// When true, trailing null entries are hidden. Used for sources that pad their array
// (e.g. pool-rented interpolated VolumeParameter values).
internal bool trimTrailingNulls = false;
}
/// <summary>
/// A read-only message box with an icon.
/// </summary>
public class MessageBox : Widget
{
#if ENABLE_RENDERING_DEBUGGER_UI
/// <inheritdoc/>
protected override VisualElement Create()
{
var helpBox = new HelpBox(displayName, (HelpBoxMessageType)style);
helpBox.text = message;
helpBox.Q(className: "unity-help-box__label").AddToClassList("debug-window-search-filter-target");
helpBox.AddToClassList("debug-window-messagebox");
if (messageCallback != null)
{
this.ScheduleTracked(helpBox, () => helpBox.schedule.Execute(() =>
{
helpBox.text = message;
}).Every(100));
}
return helpBox;
}
#endif
/// <summary>
/// Label style defines text color and background.
/// </summary>
public enum Style
{
/// <summary>
/// None category - no icon in the message
/// </summary>
None,
/// <summary>
/// Info category
/// </summary>
Info,
/// <summary>
/// Warning category
/// </summary>
Warning,
/// <summary>
/// Error category
/// </summary>
Error
}
/// <summary>
/// Style used to render displayName.
/// </summary>
public Style style = Style.Info;
/// <summary>
/// Message Callback to feed the new message to the widget
/// </summary>
public Func<string> messageCallback = null;
/// <summary>
/// This obtains the message from the display name or from the message callback if it is not null
/// </summary>
public string message => messageCallback == null ? displayName : messageCallback();
}
/// <summary>
/// Widget that will show into the Runtime UI only
/// Warning the user if the Runtime Debug Shaders variants are being stripped from the build.
/// </summary>
public class RuntimeDebugShadersMessageBox : MessageBox
{
/// <summary>
/// Constructs a <see cref="RuntimeDebugShadersMessageBox"/>
/// </summary>
public RuntimeDebugShadersMessageBox()
{
displayName =
"Warning: the debug shader variants are missing. Ensure that the \"Strip Runtime Debug Shaders\" option is disabled in the SRP Graphics Settings.";
style = DebugUI.MessageBox.Style.Warning;
isHiddenCallback = () =>
{
#if !UNITY_EDITOR
if (GraphicsSettings.TryGetRenderPipelineSettings<ShaderStrippingSetting>(out var shaderStrippingSetting))
return !shaderStrippingSetting.stripRuntimeDebugShaders;
#endif
return true;
};
}
}
}
}