Unity UI Toolkit: Reusable Panel

Introduction

UI Toolkit is the newer UI system for Unity.

Advantages over the older UI system:

  • ease of laying out the UI, with the help of UI Builder

  • ease of changing theme or style, with the unity style sheet (uss)

  • it can be used for the editor drawer as well, which reduces the headache of calculating the rect position for each property

Disadvantages

  • Documentation is not quite yet helpful at the moment, most of the problems that I encountered, were resolved only after reading from the forum where other users may have encountered similar problems.

  • Some functionality can only be known how to be done, after digging the unity source code at the github.

Disclaimer
This article assumes the readers already know the basics of the UI Toolkit. The focus will be on trying to solve the problem. As I am also new to UI Toolkit, there may be terms that I will be using wrongly. Do correct me if I do.

Problem

Create a reusable panel that can assign additional UI in the intended content container.

Scroll View
Scroll View Structure
Scroll View
Scroll View with added content

ScrollView is one such panel that can be reusable and add additional UI.

Note that, the child elements are all greyed out; no editing can be done on them. If you want to add a new UI element to it, it will have to be added to the root visual element, which in this case, would be the ScrollView. It has an internal logic that will automatically allocate the newly added UI element to the visual element, "unity-content-container".

Scroll View
Custom UI Document
Scroll View
Reusing the custom UI Document

If you try to create a UI Document and mimic the structure, and then try to use it on another UI Document, you will notice that when you try to add a new UI element, it will just be added directly under its root visual element.

Solution

I'm using Unity 2022.3.4f1.

At this point, there doesn't seem to be a way to do with it UI Document. The only way to redirect the newly added UI element is to create a new C# class that derives from VisualElement.

Steps:

  1. Create a visual element that can be shown in the Library

  2. Generate child elements via code

  3. Add styles

Create a visual element that can be shown in the Library

It is very simple to set this up.

  1. Create a class that derived from VisualElement

  2. Create an inner UxmlFactory class that is derived from UnityEngine.UIElements.UxmlFactory

public class PanelView : VisualElement
{
    public new class UxmlFactory : UnityEngine.UIElements.UxmlFactory<PanelView>
    {
    }

    public PanelView()
    {
    }
}

This is the minimum code that is needed to have it appear in the Library.

Note that the inner class has to be UxmlFactory, with the new keyword. This allows the UI Builder to know this factory is associated with PanelView in this case.

A parameterless constructor is needed as well, as new() is used when the factory tries to create it.

Go back to UI Builder, and you will see it appear under Custom Controls, from the Project tab.

Generate child elements via code

public class PanelView : VisualElement
{
    private Label _titleLabel;
    private VisualElement _contentContainer;

    // this is the important property to override
    public override VisualElement contentContainer => _contentContainer;

    public new class UxmlFactory : UnityEngine.UIElements.UxmlFactory<PanelView>
    {
    }

    public PanelView() : this("Label")
    {
    }

    public PanelView(string title)
    {
        _titleLabel = string.IsNullOrEmpty(title) ? new Label() : new Label(title);
        _titleLabel.name = "title";
        _contentContainer = new VisualElement();
        _contentContainer.name = "content-container";

        // add via base.hierachy, instead of the usual VisualElement.Add
        base.hierarchy.Add(_titleLabel);
        base.hierarchy.Add(_contentContainer);
    }
}

contentContainer is the property that is used when the Add function is called.
By overriding it, it can be changed to the intended visual element to be used as the parent to be added to.

And since contentContainer has been overridden, it is no longer safe to use the Add function in this class. As it may try to add itself to itself. The hierarchy is what is used to manage the direct children. The Add function uses it as well. By caliing hierarchy.Add directly, we can avoid adding contentContainer to contentContainer.

Now we can have the new UI element be added to the content container that we wanted.

Add styles

To prevent locking the style class name in the code, it is deliberately not to add any uss class name in the code.

As with ScrollView, the child elements will not be editable, so style cannot be added to the child elements directly. The workaround is to apply the style via the root visual element.

I'm no pro with CSS, so this is what I have tried and it seems to work well. Use ">" to redirect it with the child name, or child classes.

With this method, I can separate the logic and visuals.

Extra

Custom attributes can be added to the visual element. Attributes can be used for additional settings or information that you may need for your custom visual element.

Document for the attributes.

public class PanelView : VisualElement
{
    private Label _titleLabel { get; }
    private VisualElement _contentContainer;

    public override VisualElement contentContainer => _contentContainer;

    // this is link to the _titleText in UxmlTraits class
    // note: the naming is important, it has to be a property
    //       the name has to match the name in the attribute
    public string TitleText
    {
        get => _titleLabel.text;
        set => _titleLabel.text = value;
    }

    public new class UxmlFactory : UnityEngine.UIElements.UxmlFactory<PanelView, UxmlTraits>
    {

    }

    public new class UxmlTraits : VisualElement.UxmlTraits
    {
        // this is how you can add custom attribute to the inspector in UI Builder
        protected UxmlStringAttributeDescription _titleText = new UxmlStringAttributeDescription
        {
            name = "title-text",
            defaultValue = "Title"
        };

        public override void Init(VisualElement ve, IUxmlAttributes bag, CreationContext cc)
        {
            base.Init(ve, bag, cc);

            PanelView panelView = (PanelView)ve;

            // get the value from the bag and assigned it to the class's property
            panelView.TitleText = _titleText.GetValueFromBag(bag, cc);
        }
    }

    public PanelView() : this("Title")
    {
    }

    public PanelView(string title)
    {
        _titleLabel = string.IsNullOrEmpty(title) ? new Label() : new Label(title);
        _titleLabel.name = "title";
        _contentContainer = new VisualElement();
        _contentContainer.name = "content-container";
        base.hierarchy.Add(_titleLabel);
        base.hierarchy.Add(_contentContainer);
    }
}

A new inner UxmlTraits class, derived from VisualElement.UxmlTraits, is needed to enable adding new attributes.

Within the UxmlTraits class, any amount of attributes can be added, but limited to the types given from the document.

Override the Init function, to retrieve the attributes. Once you have the attributes, you can do whatever you want with it.

There's an important note, which is that the property variable from the visual element must exist and match the name given to the UxmlTraits's attribute (not the variable name). The reason is, that Unity uses reflection to serialize and deserialize the attribute value to the property variable. If the property variable cannot be found, the attribute value cannot be saved.

the UI Builder relies on the existence of C# attributes (ie. { get; }) to be defined on your custom element that have the same name as the corresponding attribute (same name as in, uxml uses dashes "my-value" and corresponding C# attribute uses no dashes "myValue", case insensitive).
Source

With the custom attribute, I can set my title text directly.

Conclusion

With some c# code, to create the reusable panel, I can now reuse it for different purposes.