#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 { /// /// Generic field. /// /// /// /// public class CustomRectField : DebugUI.Field<Rect> /// { /// protected override VisualElement Create() /// { /// var field = new RectField() /// { /// label = displayName, /// }; /// return field; /// } /// } /// /// /// The type of data managed by the field. public abstract class Field : Widget #pragma warning disable CS0618 // Type or member is obsolete , IValueField #pragma warning restore CS0618 // Type or member is obsolete , ISupportsLegacyStateHandling { /// /// Getter for this field. /// public Func getter { get; set; } /// /// Setter for this field. /// public Action setter { get; set; } // This should be an `event` but they don't play nice with object initializers in the // version of C# we use. /// /// Callback used when the value of the field changes. /// public Action, T> onValueChanged; /// /// Function used to validate the value when updating the field. /// /// Input value. /// Validated value. object IValueField.ValidateValue(object value) { return ValidateValue((T)value); } /// /// Function used to validate the value when updating the field. /// /// Input value. /// Validated value. public virtual T ValidateValue(T value) { return value; } /// /// Get the value of the field. /// /// Value of the field. object IValueField.GetValue() { return GetValue(); } /// /// Get the value of the field. /// /// Value of the field. public T GetValue() { Assert.IsNotNull(getter); return getter(); } /// /// Set the value of the field. /// /// Input value. public void SetValue(object value) { SetValue((T)value); } /// /// Set the value of the field. /// /// Input value. 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 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; } } /// /// Boolean field. /// public class BoolField : Field { #if ENABLE_RENDERING_DEBUGGER_UI /// protected override VisualElement Create() { var toggle = new UIElements.Toggle(); BaseFieldHelper.ConfigureBaseField(this, toggle); return toggle; } #endif } /// /// An array of checkboxes that Unity displays in a horizontal row. /// public class HistoryBoolField : BoolField { #if ENABLE_RENDERING_DEBUGGER_UI /// 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.alignedFieldUssClassName); return valueContainer; } #endif internal List childWidgets { private set; get; } = new List(); /// /// History getter for this field. /// public Func[] historyGetter { get; set; } /// /// Depth of the field's history. /// public int historyDepth => historyGetter?.Length ?? 0; /// /// Get the value of the field at a certain history index. /// /// Index of the history to query. /// Value of the field at the provided history index. 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](); } } /// /// A slider for an integer. /// public class IntField : Field { /// /// Minimum value function. /// public Func min; /// /// Maximum value function. /// public Func max; // Runtime-only /// /// Step increment. /// public int incStep = 1; /// /// Step increment multiplier. /// [Obsolete("Use incStepMult instead #from(6000.5) (UnityUpgradable) -> incStepMult")] public int intStepMult = 10; /// /// Step increment multiplier. /// public int incStepMult = 10; /// /// Function used to validate the value when updating the field. /// /// Input value. /// Validated value. 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 /// 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 } /// /// A slider for a positive integer. /// public class UIntField : Field { /// /// Minimum value function. /// public Func min; /// /// Maximum value function. /// public Func max; // Runtime-only /// /// Step increment. /// public uint incStep = 1u; /// /// Step increment multiplier. /// [Obsolete("Use incStepMult instead #from(6000.5) (UnityUpgradable) -> incStepMult")] public uint intStepMult = 10u; /// /// Step increment multiplier. /// public uint incStepMult = 10u; /// /// Function used to validate the value when updating the field. /// /// Input value. /// Validated value. 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 /// protected override VisualElement Create() { var field = new UIElements.UnsignedIntegerField(); if (m_Context.IsAnyRuntimeContext()) { field.RegisterCallback(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>((evt) => { field.SetValueWithoutNotify(ValidateValue(evt.newValue)); }); return field; } #endif } /// /// A slider for a float. /// public class FloatField : Field { /// /// Minimum value function. /// public Func min; /// /// Maximum value function. /// public Func max; // Runtime-only /// /// Step increment. /// public float incStep = 0.1f; /// /// Step increment multiplier. /// public float incStepMult = 10f; /// /// Number of decimals. /// public int decimals = 3; /// /// Function used to validate the value when updating the field. /// /// Input value. /// Validated value. 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); } /// 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 } /// /// Field that displays /// public class RenderingLayerField : Field, 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(); private int m_DefinedRenderingLayersCount = -1; private int maxRenderingLayerCount { get { #if UNITY_EDITOR if (UnityEditor.Rendering.EditorGraphicsSettings. TryGetFirstRenderPipelineSettingsFromInterface(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(m_RenderingLayersNames), 0); maskField.labelElement.AddToClassList("debug-window-search-filter-target"); maskField.RegisterCallback>(evt => { SetValue(evt.newValue); }); this.ScheduleTracked(maskField, () => maskField.schedule.Execute(() => { var value = GetValue(); maskField.SetValueWithoutNotify(Convert.ToInt32(value)); }) .Every(100)); maskField.AddToClassList(UIElements.BaseField.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(); } /// /// Obtains the list of the available rendering layer names /// public string[] renderingLayersNames { get { if (m_DefinedRenderingLayersCount != RenderingLayerMask.GetDefinedRenderingLayerCount()) { Resize(); } return m_RenderingLayersNames; } } private ObservableList m_RenderingLayersColors = new ObservableList(); /// /// Gets the list of widgets representing the rendering layer colors. /// public ObservableList children { get { if (m_DefinedRenderingLayersCount != RenderingLayerMask.GetDefinedRenderingLayerCount()) { Resize(); } return m_RenderingLayersColors; } } /// /// Obtains the color in a given index /// public Func getRenderingLayerColor { get; set; } /// /// Sets the color for a given index /// public Action setRenderingLayerColor { get; set; } internal override void GenerateQueryPath() { base.GenerateQueryPath(); int numChildren = children.Count; for (int i = 0; i < numChildren; i++) children[i].GenerateQueryPath(); } } /// /// Generic that stores enumNames and enumValues /// /// The inner type of the field public abstract class EnumField : Field { #if ENABLE_RENDERING_DEBUGGER_UI /// protected override VisualElement Create() { var field = new UIElements.PopupField() { 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.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 /// /// List of names of the enumerator entries. /// public GUIContent[] enumNames; private int[] m_EnumValues; /// /// List of values of the enumerator entries. /// 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); /// /// Automatically fills the enum names with a given /// /// The enum type /// Whether the item with the value zero should be removed from enumNames and enumValues 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.Get(out var tmpNames)) using (ListPool.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(); 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(evt => { var inputElement = popupField.Q(className: "unity-base-field__input"); if (inputElement != null) inputElement.style.backgroundColor = hoverColor; }); popupField.RegisterCallback(evt => { var inputElement = popupField.Q(className: "unity-base-field__input"); if (inputElement != null) inputElement.style.backgroundColor = new StyleColor(StyleKeyword.Null); }); } } #endif /// /// A dropdown that contains the values from an enum. /// public class EnumField : EnumField { #if ENABLE_RENDERING_DEBUGGER_UI /// protected override VisualElement Create() { var field = new UIElements.PopupField() { 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.alignedFieldUssClassName); HackPopupHoverColor(field, m_Context); field.RegisterCallback>(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(); /// /// Get the enumeration value index. /// public Func getIndex { get; set; } /// /// Set the enumeration value index. /// public Action setIndex { get; set; } /// /// Current enumeration value index. /// public int currentIndex { get => getIndex(); set => setIndex(value); } private Type m_Type; /// /// Generates enumerator values and names automatically based on the provided type. /// 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++; } } /// /// Set the value of the field. /// /// Input value. 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; } } } /// /// A dropdown that contains a list of Unity objects. /// public class ObjectPopupField : Field { #if ENABLE_RENDERING_DEBUGGER_UI /// protected override VisualElement Create() { var choices = new List() { null }; choices.AddRange(getObjects()); var field = new UIElements.PopupField() { 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>(evt => { SetValue(evt.newValue); }); this.ScheduleTracked(field, () => field.schedule.Execute(() => { field.SetValueWithoutNotify(GetValue()); }).Every(100)); field.AddToClassList(UIElements.BaseField.alignedFieldUssClassName); return field; } #endif /// /// Callback to obtain the elemtents of the pop up /// public Func> getObjects { get; set; } } /// /// A dropdown that contains a list of cameras /// public class CameraSelector : ObjectPopupField { /// /// A dropdown that contains a list of cameras /// public CameraSelector() { displayName = "Camera"; getObjects = () => cameras; } private Camera[] m_CamerasArray; private List m_Cameras = new List(); #if ENABLE_RENDERING_DEBUGGER_UI /// protected override VisualElement Create() { var objectPopUpField = base.Create() as UIElements.PopupField; if (objectPopUpField == null) return new Label("Error creating CameraSelector field"); objectPopUpField.choices ??= new List() { 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.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 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(out _)) m_Cameras.Add(camera); } } return m_Cameras; } } } /// /// Enumerator field with history. /// public class HistoryEnumField : EnumField { #if ENABLE_RENDERING_DEBUGGER_UI /// 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 childWidgets { private set; get; } = new List(); /// /// History getter for this field. /// public Func[] historyIndexGetter { get; set; } /// /// Depth of the field's history. /// public int historyDepth => historyIndexGetter?.Length ?? 0; /// /// Get the value of the field at a certain history index. /// /// Index of the history to query. /// Value of the field at the provided history index. 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](); } } /// /// Bitfield enumeration field. /// public class BitField : EnumField { #if ENABLE_RENDERING_DEBUGGER_UI /// 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>(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.alignedFieldUssClassName); m_AdditionalSearchText = string.Join(",", enumNameStrings); return maskField; } #endif Type m_EnumType; /// /// Generates bitfield values and names automatically based on the provided type. /// 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 { 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 /// /// Color field. /// public class ColorField : Field { #if ENABLE_RENDERING_DEBUGGER_UI /// protected override VisualElement Create() { UIElements.BaseField 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.alignedFieldUssClassName); field.RegisterCallback>(evt => SetValue(evt.newValue)); this.ScheduleTracked(field, () => field.schedule.Execute(() => { field.SetValueWithoutNotify((Color)Convert.ChangeType(GetValue(), typeof(Color))); }).Every(100)); } return field; } #endif /// /// HDR color. /// public bool hdr = false; /// /// Show alpha of the color field. /// public bool showAlpha = true; // Editor-only /// /// Show the color picker. /// public bool showPicker = true; // Runtime-only /// /// Step increment. /// public float incStep = 0.025f; /// /// Step increment multiplier. /// public float incStepMult = 5f; /// /// Number of decimals. /// public int decimals = 3; /// /// Function used to validate the value when updating the field. /// /// Input value. /// Validated value. 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 : UIElements.BaseField where T : struct { public RuntimeBaseFieldWrapper(VisualElement container, string label) : base(label, container) { } } #endif /// /// Generic base class for vector fields with stepper support for Runtime. /// /// The numeric vector type public abstract class VectorField : DebugUI.Field where T : struct { // Runtime-only /// /// Step increment. /// public float incStep = 0.025f; /// /// Step increment multiplier. /// public float incStepMult = 10f; /// /// Number of decimals. /// public int decimals = 3; #if ENABLE_RENDERING_DEBUGGER_UI /// /// Selected component index. /// internal int selectedComponent = -1; /// protected override VisualElement Create() { UIElements.BaseField vectorField; if (m_Context.IsAnyRuntimeContext()) { vectorField = CreateRuntimeVectorField(); } else { vectorField = CreateEditorVectorField(); } BaseFieldHelper.ConfigureBaseField(this, vectorField); return vectorField; } private UIElements.BaseField 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(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(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 CreateEditorVectorField(); #endif } /// /// Vector2 field. /// public class Vector2Field : VectorField { #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 CreateEditorVectorField() => new UIElements.Vector2Field(); #endif } /// /// Vector3 field. /// public class Vector3Field : VectorField { #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 CreateEditorVectorField() => new UIElements.Vector3Field(); #endif } /// /// Vector4 field. /// public class Vector4Field : VectorField { #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 CreateEditorVectorField() => new UIElements.Vector4Field(); #endif } #if ENABLE_RENDERING_DEBUGGER_UI private class RuntimeObjectField : UIElements.BaseField { 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 /// /// A field for selecting a Unity object. /// public class ObjectField : Field { #if ENABLE_RENDERING_DEBUGGER_UI /// protected override VisualElement Create() { UIElements.BaseField 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.alignedFieldUssClassName); field.RegisterCallback>(evt => SetValue(evt.newValue)); this.ScheduleTracked(field, () => field.schedule.Execute(() => { var selectedObject = GetValue(); field.SetValueWithoutNotify(selectedObject); }).Every(100)); return field; } #endif /// /// Object type. /// public Type type = typeof(Object); } /// /// A list of fields for selecting Unity objects. /// public class ObjectListField : Field { #if ENABLE_RENDERING_DEBUGGER_UI /// 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 field; if (i < container.childCount) { field = (BaseField)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 /// /// Objects type. /// 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; } /// /// A read-only message box with an icon. /// public class MessageBox : Widget { #if ENABLE_RENDERING_DEBUGGER_UI /// 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 /// /// Label style defines text color and background. /// public enum Style { /// /// None category - no icon in the message /// None, /// /// Info category /// Info, /// /// Warning category /// Warning, /// /// Error category /// Error } /// /// Style used to render displayName. /// public Style style = Style.Info; /// /// Message Callback to feed the new message to the widget /// public Func messageCallback = null; /// /// This obtains the message from the display name or from the message callback if it is not null /// public string message => messageCallback == null ? displayName : messageCallback(); } /// /// Widget that will show into the Runtime UI only /// Warning the user if the Runtime Debug Shaders variants are being stripped from the build. /// public class RuntimeDebugShadersMessageBox : MessageBox { /// /// Constructs a /// 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(out var shaderStrippingSetting)) return !shaderStrippingSetting.stripRuntimeDebugShaders; #endif return true; }; } } } }