#if ENABLE_UIELEMENTS_MODULE && UNITY_ENABLE_CHECKS #define ENABLE_RENDERING_DEBUGGER_UI #endif using System; using System.Collections.Generic; #if UNITY_EDITOR using UnityEditor; #endif #if ENABLE_RENDERING_DEBUGGER_UI using UnityEngine.UIElements; #endif namespace UnityEngine.Rendering { public partial class DebugUI { /// /// Base class for "container" type widgets, although it can be used on its own (if a display name is set then it'll behave as a group with a header) /// public class Container : Widget, IContainer { #if ENABLE_RENDERING_DEBUGGER_UI /// protected override VisualElement Create() { VisualElement container = new UIElements.VisualElement(); container.AddToClassList("debug-window-container"); if (!hideDisplayName) { var label = new Label(displayName); label.AddToClassList("debug-window-container-displayname"); label.AddToClassList("debug-window-search-filter-target"); container.Add(label); } var content = new VisualElement(); content.AddToClassList("debug-window-container-content"); AddChildren(content, m_Context); container.Add(content); return container; } /// /// Adds all the children to the given container /// /// The container to insert elements /// The context where the widget is being added protected void AddChildren(VisualElement container, DebugUI.Context context) { for (int i = 0; i < children.Count; ++i) { var child = children[i]; if (context == Context.Editor && child.isRuntimeOnly) continue; if (context.IsAnyRuntimeContext() && child.isEditorOnly) continue; var childUIElement = child.ToVisualElement(context); if (childUIElement != null) container.Add(childUIElement); } // As the elements might change visibility, we need to update alternating background colors if (alternateRowColors) { this.ScheduleTracked(container, () => container.schedule.Execute(() => { int i = 0; foreach (var child in container.Children()) { if (child.style.display == DisplayStyle.Flex) { if (i % 2 != 0) child.AddToClassList(k_AlternateRowColorClassName); else child.RemoveFromClassList(k_AlternateRowColorClassName); ++i; } } }).Every(100)); } } #endif const string k_IDToken = "#"; internal bool hideDisplayName => string.IsNullOrEmpty(displayName) || displayName.StartsWith(k_IDToken); /// /// List of children. /// public ObservableList children { get; private set; } const string k_AlternateRowColorClassName = "debug-window-row-alternate"; /// /// Set to true to alternate background colors of the rows inside the container. /// public bool alternateRowColors { get; set; } /// /// Panel the container is attached to. /// public override Panel panel { get { return m_Panel; } internal set { m_Panel = value; // Bubble down int numChildren = children.Count; for (int i = 0; i < numChildren; i++) children[i].panel = value; } } /// /// Constructor /// public Container() : this(string.Empty, new ObservableList()) { } /// /// Constructor for a container without header /// /// The id of the container public Container(string id) : this($"{k_IDToken}{id}", new ObservableList()) { } /// /// Constructor. /// /// Display name of the container. /// List of attached children. public Container(string displayName, ObservableList children) { this.displayName = displayName; this.children = children; children.ItemAdded += OnItemAdded; children.ItemRemoved += OnItemRemoved; // Call OnAdded callback for already existing items to ensure their panel & parent are set for (int i = 0; i < this.children.Count; i++) OnItemAdded(this.children, new ListChangedEventArgs(i, this.children[i])); } internal override void GenerateQueryPath() { base.GenerateQueryPath(); int numChildren = children.Count; for (int i = 0; i < numChildren; i++) children[i].GenerateQueryPath(); } /// /// Method called when a children is added. /// /// Sender widget. /// List of added children. protected virtual void OnItemAdded(ObservableList sender, ListChangedEventArgs e) { if (e.item != null) { e.item.panel = m_Panel; e.item.parent = this; } if (m_Panel != null) m_Panel.SetDirty(); } /// /// Method called when a children is removed. /// /// Sender widget. /// List of removed children. protected virtual void OnItemRemoved(ObservableList sender, ListChangedEventArgs e) { if (e.item != null) { e.item.panel = null; e.item.parent = null; } if (m_Panel != null) m_Panel.SetDirty(); } } /// /// Unity-like foldout that can be collapsed. /// public class Foldout : Container #pragma warning disable CS0618 // Type or member is obsolete , IValueField #pragma warning restore CS0618 // Type or member is obsolete { #if ENABLE_RENDERING_DEBUGGER_UI /// protected override VisualElement Create() { var container = new UIElements.Foldout { text = displayName }; if (m_Context == Context.Editor && (contextMenuItems != null || !string.IsNullOrEmpty(documentationUrl))) { #if UNITY_EDITOR // Get Toggle element of the Foldout to be able to attach the help and menu buttons var toggleHeader = container.Q().ElementAt(0); if (!string.IsNullOrEmpty(documentationUrl)) { var infoButton = new UIElements.Button((Background.FromTexture2D((Texture2D)EditorGUIUtility.TrIconContent("_Help").image )),() => Help.BrowseURL(documentationUrl)); infoButton.AddToClassList("info-button"); infoButton.tooltip = $"Open Reference for {displayName}."; toggleHeader.Add(infoButton); } if (contextMenuItems != null) { var button = new UIElements.Button(); button.AddToClassList("more-options-button"); var contextualMenu = new GenericDropdownMenu(); foreach(var item in contextMenuItems) { contextualMenu.AddItem(item.displayName, false, item.action ); } button.clicked += () => { contextualMenu.DropDown(button.worldBound, button, DropdownMenuSizeMode.Auto); }; toggleHeader.Add(button); } #endif } // Add special classes for styles // Use header style for top-level foldouts (not inside a Container), regular style for nested ones bool isHeaderFoldout = parent is DebugUI.Panel; container.AddToClassList(isHeaderFoldout ? "debug-window-foldout-header" : "debug-window-foldout"); container.Q(className: "unity-foldout__text").AddToClassList("debug-window-search-filter-target"); // Update UI to match object's initial state container.SetValueWithoutNotify(GetValue()); // Sync value to object when changed in UI container.RegisterCallback>(evt => { SetValue(container.value); }); // Sync value to object based on navigation events separately, because they don't trigger a ChangeEvent container.RegisterCallback(evt => { if (DebugManager.instance.selectedWidget == this) { if (evt.direction == NavigationMoveEvent.Direction.Right) SetValue(true); else if (evt.direction == NavigationMoveEvent.Direction.Left) SetValue(false); } }, TrickleDown.TrickleDown); // Foldout internally stops propagation so must use TrickleDown to see it // Sync opened state from object to UI this.ScheduleTracked(container, () => container.schedule.Execute(() => { container.SetValueWithoutNotify(GetValue()); }).Every(100)); if (columnLabels is { Length: > 0 }) { var columnLabelsValues = new DebugUI.Value[columnLabels.Length]; for (int i = 0; i < columnLabels.Length; i++) { string value = columnLabels[i]; object Getter() => value; columnLabelsValues[i] = new DebugUI.Value() { displayName = string.Empty, getter = Getter }; } var header = new DebugUI.ValueTuple() { displayName = string.Empty, isHeader = true, values = columnLabelsValues }; container.Add(header.ToVisualElement(m_Context)); container.RegisterCallback(evt => { var numColumns = columnLabels.Length; foreach (var visualElement in container.Children()) { var width = (evt.newRect.width - ValueTuple.GetLabelWidth(m_Context)) / numColumns; int i = 0; foreach (var child in visualElement.Children()) { if (i != 0) child.style.width = width; ++i; } } }); } AddChildren(container, m_Context); return container; } #endif /// /// Context menu item. /// public struct ContextMenuItem { /// /// Name of the item displayed in context menu dropdown. /// public string displayName; /// /// Callback when context menu item is selected. /// public Action action; } /// /// Always false. /// public bool isReadOnly { get { return false; } } bool m_Opened; /// /// Opened state of the foldout. /// public bool opened { get => m_Opened; set { if (opened != value) { #if ENABLE_RENDERING_DEBUGGER_UI if (m_VisualElement != null) DebugManager.instance.schedulerTracker.SetHierarchyEnabled(m_Context, this, value); #endif m_Opened = value; } } } /// /// Draw the foldout in full width using a header style. /// public bool isHeader; /// /// Optional list of context menu items. If the list is not provided, no context menu button will be displayed. /// public List contextMenuItems = null; /// /// Optional help URL for the editor UI /// public string documentationUrl { get; set; } private bool m_Dirty; private string[] m_ColumnLabels; private string[] m_ColumnTooltips; /// /// List of columns labels. /// public string[] columnLabels { get => m_ColumnLabels; set { m_ColumnLabels = value; m_Dirty = true; } } /// /// List of columns label tooltips. /// public string[] columnTooltips { get => m_ColumnTooltips; set { m_ColumnTooltips = value; m_Dirty = true; } } private List m_RowContents = new(); internal List rowContents { get { if (m_Dirty) { if (m_ColumnTooltips == null) { m_ColumnTooltips = new string[m_ColumnLabels.Length]; Array.Fill(columnTooltips, string.Empty); } else { if (m_ColumnTooltips.Length != m_ColumnLabels.Length) throw new Exception( $"Dimension for labels and tooltips on {nameof(DebugUI.Foldout)} - {displayName}, do not match"); } m_RowContents.Clear(); for (int i = 0; i < m_ColumnLabels.Length; ++i) { string label = columnLabels[i] ?? string.Empty; string tooltip = m_ColumnTooltips[i] ?? string.Empty; m_RowContents.Add( #if UNITY_EDITOR EditorGUIUtility.TrTextContent(label, tooltip) #else new GUIContent(label, tooltip) #endif ); } m_Dirty = false; } return m_RowContents; } } /// /// Constructor. /// public Foldout() : base() { } /// /// Constructor. /// /// Display name of the foldout. /// List of attached children. /// Optional list of column names. /// Optional list of tooltips for column name labels. public Foldout(string displayName, ObservableList children, string[] columnLabels = null, string[] columnTooltips = null) : base(displayName, children) { this.columnLabels = columnLabels; this.columnTooltips = columnTooltips; } /// /// Get the opened state of the foldout. /// /// True if the foldout is opened. public bool GetValue() => opened; /// /// Get the opened state of the foldout. /// /// True if the foldout is opened. object IValueField.GetValue() => GetValue(); /// /// Set the opened state of the foldout. /// /// True to open the foldout, false to close it. [Obsolete("This method is no longer used. #from(6000.5)")] public void SetValue(object value) => SetValue((bool)value); /// /// Validates the value of the widget before setting it. /// /// Input value. /// The validated value. [Obsolete("This method is no longer used. #from(6000.5)")] public object ValidateValue(object value) => value; /// /// Set the value of the widget. /// /// Input value. public void SetValue(bool value) => opened = value; } /// /// Horizontal Layout Container. /// public class HBox : Container { /// /// Constructor. /// public HBox() { displayName = "HBox"; } } /// /// Vertical Layout Container. /// public class VBox : Container { /// /// Constructor. /// public VBox() { displayName = "VBox"; } } /// /// Array Container. /// public class Table : Container { #if ENABLE_RENDERING_DEBUGGER_UI /// protected override VisualElement Create() { var container = new UIElements.VisualElement(); container.AddToClassList("debug-window-table"); using (ListPool.Get(out var tmp)) { if (children.Count != 0 && children[0] is Row row) { foreach (var child in row.children) tmp.Add(child.displayName); } var columnLabelsValues = new DebugUI.Value[tmp.Count]; for (int i = 0; i < columnLabelsValues.Length; i++) { string value = tmp[i]; object Getter() => value; columnLabelsValues[i] = new DebugUI.Value() { displayName = string.Empty, getter = Getter }; } var header = new DebugUI.ValueTuple() { displayName = string.Empty, isHeader = true, values = columnLabelsValues }; var headerVisualElement = header.ToVisualElement(m_Context); headerVisualElement.AddToClassList("debug-window-table-header-row"); foreach (var headerElement in headerVisualElement.Children()) headerElement.AddToClassList("debug-window-table-header-cell"); container.Add(headerVisualElement); // NOTE: Current implementation constructs the table cells from individual elements, so cell width needs to be // set manually like this. This is necessary because of the DebugUI.Table/Row structure, which is why we can't use // a proper table. container.RegisterCallback(evt => { var numColumns = columnLabelsValues.Length; var availableWidth = evt.newRect.width; if (displayRowNames) availableWidth -= ValueTuple.GetLabelWidth(m_Context); var width = availableWidth / numColumns; foreach (var visualElement in container.Children()) { int i = 0; foreach (var child in visualElement.Children()) { if (i == 0) child.style.display = displayRowNames ? DisplayStyle.Flex : DisplayStyle.None; else { child.style.width = width; child.style.display = DisplayStyle.Flex; } ++i; } } }); } AddChildren(container, m_Context); container.enabledSelf = !isReadOnly; return container; } #endif /// Row Container. public class Row : Container { /// Constructor. public Row() { displayName = "Row"; } /// Opened state of the row. No longer used. [Obsolete("This class no longer inherits from Foldout, but from Container. This field is kept for compatibility but it no longer does anything. #from(6000.5)")] public bool opened { get; set; } #if ENABLE_RENDERING_DEBUGGER_UI /// protected override VisualElement Create() { var rowContainer = new UIElements.VisualElement(); rowContainer.AddToClassList("debug-window-table-row"); var label = new Label(displayName) { style = { width = ValueTuple.GetLabelWidth(m_Context) }, }; label.AddToClassList("debug-window-table-row-displayname"); label.AddToClassList("debug-window-search-filter-target"); rowContainer.Add(label); foreach (var c in children) { c.keepLayoutWhenHidden = true; var rowElement = c.ToVisualElement(m_Context); rowElement.AddToClassList("debug-window-table-cell"); // In Table layout we don't want to show the labels of DebugUI.Values var valueLabel = rowElement.Q