#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