Properties UI
The com.unity.properties.ui
package offers utilities to generate UIToolkit-based visual trees from data using the com.unity.properties
package. This API is found in the Unity.Properties.UI.Editor
assembly (referenced automatically for convenience), under the Unity.Properties.UI
namespace.
The main goal of this package is to allow:
- the creation of generic inspector-like visual trees,
- the creation of custom inspectors and property drawers
- data-binding
- enhanced support for collection types in the editor
This manual targets developers that intends to create custom inspectors.
Glossary
Inspectors are drawers based on a given type. These drawers will be used for the root instance.
Property Inspectors are drawers based on a given type. These drawers will be used for every field of that type.
Property Attribute Inspectors are drawers based on an attribute. Theses drawers will be used when a field is tagged with a PropertyAttribute
. This allows to draw fields of heterogeneous types in a similar fashion.
Getting Started
Here are short examples illustrating a basic usage of the Properties UI API.
Creating a simple editor window
namespace Unity.Properties.Samples
{
using Unity.Properties.UI;
using UnityEditor;
using UnityEngine;
public class SampleWindow : EditorWindow
{
public class WindowVersion
{
public int Major = 1, Minor = 0, Patch = 0;
}
public class WindowData
{
public string Description = "Sample window using Properties UI";
public WindowVersion Version = new WindowVersion();
}
[MenuItem("Properties/Samples/Sample Window")]
public static void ShowExample()
{
var wnd = GetWindow<SampleWindow>();
wnd.titleContent = new GUIContent("SampleWindow");
}
public void OnEnable()
{
var inspectorElement = new InspectorElement();
inspectorElement.SetTarget(new WindowData());
rootVisualElement.Add(inspectorElement);
}
}
}
Creating an inspector
namespace Unity.Properties.Samples
{
using Unity.Properties.UI;
using UnityEngine.UIElements;
class WindowDataInspector : Inspector<SampleWindow.WindowData>
{
public override VisualElement Build()
{
var windowData = Target;
var root = DoDefaultGui();
root.style.backgroundColor = new Color(46/255.0f, 46/255.0f, 46/255.0f);
return root;
}
}
}
Creating a property inspector
namespace Unity.Properties.Samples
{
using Unity.Properties.UI;
using UnityEngine.UIElements;
class WindowVersionPropertyInspector : PropertyInspector<SampleWindow.WindowVersion>
{
public override VisualElement Build()
{
var version = Target;
var valueLabel = new Label {text = $"Version {version.Major}.{version.Minor}.{version.Patch}"};
valueLabel.style.alignSelf = Align.FlexEnd;
return valueLabel;
}
}
}
Creating a property attribute inspector
namespace Unity.Properties.Samples
{
using Unity.Properties.UI;
using UnityEngine;
using UnityEngine.UIElements;
public class CenteredLabelAttribute : PropertyAttribute
{
public int FontSize = 16;
}
public class CenteredLabelAttributeInspector : PropertyInspector<string, CenteredLabelAttribute>
{
public override VisualElement Build()
{
var attribute = DrawerAttribute;
var label = new Label {text = Target};
label.style.fontSize = attribute.FontSize;
label.style.alignSelf = Align.Center;
return label;
}
}
}
public class WindowData
{
[CenteredLabel(FontSize = 24)]
public string Description = "Sample window using Properties UI";
public WindowVersion Version = new WindowVersion();
}
Built-in data-binding
namespace Unity.Properties.Samples
{
using Unity.Properties.UI;
using UnityEngine.UIElements;
using UnityEditor;
using UnityEngine;
public class DataBindingSampleWindow : EditorWindow
{
public class Data
{
public int Int;
}
[CreateProperty] Data m_Data;
[MenuItem("Properties/Samples/Data-binding Sample Window")]
public static void ShowExample()
{
var wnd = GetWindow<DataBindingSampleWindow>();
wnd.titleContent = new GUIContent("Data Binding Sample");
}
public void OnEnable()
{
m_Data = new Data();
var propertyElement = new InspectorElement();
propertyElement.SetTarget(m_Data);
rootVisualElement.Add(propertyElement);
rootVisualElement.Add(new Button(IncrementValue){ text = "Increment value"});
}
void IncrementValue()
{
m_Data.Int++;
}
}
}
Manual data-binding
using Unity.Properties.UI;
using UnityEditor.UIElements;
using UnityEngine.UIElements;
namespace Unity.Properties.Samples
{
public class DataBindingInspector : Inspector<DataBindingSampleWindow.Data>
{
Label m_ValueAsLabel;
public override VisualElement Build()
{
var root = new VisualElement();
var fieldName = nameof(DataBindingSampleWindow.Data.Int);
var integerField = new IntegerField(fieldName);
integerField.style.flexGrow = 1;
// using the binding path will automatically update the value whenever possible.
integerField.bindingPath = fieldName;
root.Add(integerField);
// there is no automatic binding between int => label, so we will use the Update method to update the
// text of the label to match the value.
m_ValueAsLabel = new Label(Target.Int.ToString());
m_ValueAsLabel.style.alignSelf = Align.Center;
m_ValueAsLabel.style.width = 75;
root.Add(m_ValueAsLabel);
root.style.flexDirection = FlexDirection.Row;
return root;
}
public override void Update()
{
m_ValueAsLabel.text = Target.Int.ToString();
}
}
}
These samples focus on code-driven workflows. It is possible to use uxml
and uss
files to drive the look and feel of a custom inspector or property drawer. Setting the binding-path
attribute in uxml
files will kick-off automatic data-binding.
Uxml-based data-binding
Editor Window
using Unity.Properties.UI;
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;
namespace Unity.Properties.Samples
{
public class CardEditorWindow : EditorWindow
{
[MenuItem("Properties/Samples/Card Editor")]
public static void ShowExample()
{
var wnd = GetWindow<CardEditorWindow>();
wnd.titleContent = new GUIContent("Card Editor");
}
struct CharacterInfo
{
public string Name;
public float Level;
}
struct Stats
{
public float Life;
public float Armor;
}
class Character
{
public CharacterInfo Info;
public Texture2D Texture;
public string Description;
public Stats Stats;
}
class UICard
{
public Character Character;
}
class CharacterInspector : Inspector<UICard>
{
public override VisualElement Build()
{
var root = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>("Assets/Samples/3. EditorWindow/Editor/card.uxml").Instantiate();
var styleSheet = AssetDatabase.LoadAssetAtPath<StyleSheet>("Assets/Samples/3. EditorWindow/Editor/card.uss");
root.styleSheets.Add(styleSheet);
return root;
}
}
public void OnEnable()
{
// Setup the character data.
var character = new Character
{
Texture = AssetDatabase.LoadAssetAtPath<Texture2D>("Assets/Samples/3. EditorWindow/Editor/Ellen_CutscenePortrait_000.png"),
Info = new CharacterInfo { Name = "Ellen", Level = 20 },
Description = "Kick-ass adventurer that laughs in the face of danger",
Stats = new Stats { Life = 250, Armor = 500 }
};
var card = new UICard {Character = character};
var cardElement = new InspectorElement();
cardElement.SetTarget(card);
rootVisualElement.Add(cardElement);
}
}
}
UXML file
<?xml version="1.0" encoding="utf-8"?>
<engine:UXML
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:engine="UnityEngine.UIElements"
xmlns:editor="UnityEditor.UIElements"
xsi:noNamespaceSchemaLocation="../../../../UIElementsSchema/UIElements.xsd"
>
<engine:BindableElement binding-path="Character">
<engine:VisualElement class="self-centered card-info thin-border">
<engine:BindableElement class="card-character regular-border" binding-path="Texture"/>
<engine:BindableElement class="row self-centered" binding-path="Info">
<engine:Label class="big-label" binding-path="Name"/>
<engine:Label class="big-label" binding-path="Level"/>
</engine:BindableElement>
<engine:VisualElement class="card-info regular-border">
<engine:Label text="Description" class="self-centered bold-title"/>
<engine:Label class="card-description" binding-path="Description"/>
<engine:Label text="Stats" class="self-centered bold-title"/>
<engine:BindableElement binding-path="Stats">
<engine:Label text="Life" class="bold-title"/>
<engine:Label class="card-stats" binding-path="Life"/>
</engine:BindableElement>
<engine:BindableElement binding-path="Stats">
<engine:Label text="Armor" class="bold-title"/>
<engine:Label class="card-stats" binding-path="Armor"/>
</engine:BindableElement>
</engine:VisualElement>
</engine:VisualElement>
</engine:BindableElement>
</engine:UXML>
USS file
.row
{
flex-direction: row;
}
.thin-border
{
border-width: 1px;
border-radius: 5px;
}
.regular-border
{
border-width: 3px;
border-radius: 5px;
}
.thick-border
{
border-width: 5px;
border-radius: 5px;
}
.big-label
{
font-size: 26px;
}
.self-centered
{
align-self: center;
}
.bold-title
{
font-size: 14px;
padding-bottom: 5px;
-unity-font-style: bold;
}
.card-character
{
background-color: lightskyblue;
width:256px;
height:256px;
border-color: gold;
}
.card-frame
{
background-color: rgba(0, 0, 0, 0.25);
width:264px;
height:512px;
border-color: black;
}
.card-info
{
background-color: rgba(0, 0, 0, 0.25);
margin-left: 25px;
margin-right: 25px;
margin-bottom: 25px;
border-color: black;
}
.card-description
{
font-size: 10px;
margin-left: 5px;
margin-right: 5px;
flex-wrap: wrap;
-unity-font-style: bold-and-italic;
white-space: normal;
padding-bottom: 8px;
background-color: #d4d4d4;
color:black;
}
.card-stats
{
font-size: 10px;
margin-left: 5px;
margin-right: 128px;
flex-wrap: wrap;
-unity-font-style: bold-and-italic;
white-space: normal;
margin-bottom: 8px;
border-left-width: 2px;
border-color: black;
border-bottom-width: 1px;
background-color: #d4d4d4;
color:black;
}
Performance Considerations
InspectorElement & PropertyElement
Unity.Properties.UI.InspectorElement
and Unity.Properties.UI.PropertyElement
will generate new VisualElement
instances whenever a new target is set, which can result in heavy allocation-spikes for large data set. We're looking at ways to pool and reuse visual trees so that these spikes occur less often.
Other Considerations
Multi-selection
At the moment, it is not possible to bind multiple targets to a Unity.Properties.UI.InspectorElement
or a Unity.Properties.UI.PropertyElement
.