UI Toolbox logoUI Toolbox

Data-bound windows

WindowDescriptor.DataSource holds the data that a window shows. The window sets it as the UI Toolkit dataSource of its content. Bindings in the content resolve against it, also after the window moves into a tab group or a dock.

One content template, many windows:

<!-- Resources/UI/ItemDetail.uxml -->
<ui:UXML xmlns:ui="UnityEngine.UIElements">
  <ui:Label>
    <Bindings>
      <ui:DataBinding property="text" data-source-path="Name" binding-mode="ToTarget" />
    </Bindings>
  </ui:Label>
</ui:UXML>
public class Item
{
    public string Id = string.Empty;
    public string Name = string.Empty;
}

public static class ItemWindows
{
    private static readonly VisualTreeAsset Template = Resources.Load<VisualTreeAsset>("UI/ItemDetail");

    // Opens the item's window, or focuses it when it is already registered
    public static void Show(Workspace workspace, Item item)
    {
        var id = $"Item_{item.Id}";
        if (workspace.GetWindow(id) == null)
        {
            WindowDescriptor.Builder()
                .WithId(id)
                .WithTitle(item.Name)
                .WithContent(Template.Instantiate())
                .WithDataSource(item)
                .BuildAndRegister(workspace);
        }
        workspace.OpenWindow(id);
    }

    // Queries use GetAllWindows
    public static WindowDescriptor? Find(Workspace workspace, Item item) =>
        workspace.GetAllWindows().FirstOrDefault(w => w.DataSource == item);

    public static IEnumerable<WindowDescriptor> All(Workspace workspace) =>
        workspace.GetAllWindows().Where(w => w.DataSource is Item);
}

Binding converters

ToolboxConverters registers converter groups for conversions that Unity does not have. A binding opts in by the group id, in UXML or in UI Builder:

<ui:VisualElement>
  <Bindings>
    <ui:DataBinding property="style.display" data-source-path="IsAlive" binding-mode="ToTarget" source-to-ui-converters="tb:visible" />
  </Bindings>
</ui:VisualElement>
<ui:RadioButtonGroup choices="Easy,Normal,Hard">
  <Bindings>
    <ui:DataBinding property="value" data-source-path="Difficulty" binding-mode="TwoWay"
                    source-to-ui-converters="tb:enum-int" ui-to-source-converters="tb:enum-int" />
  </Bindings>
</ui:RadioButtonGroup>

A group has no parameters, so the id says what it does. A group matches the declared type of the source exactly, with no base types. Groups do not chain.

Group Source UI Result
tb:visible, tb:hidden bool DisplayStyle, style.display True shows (Flex) or hides (None). tb:hidden inverts.
tb:visibility, tb:invisibility bool Visibility, style.visibility Same, but the element keeps its space.
tb:not bool bool Inverts, both ways.
tb:has-value string, object, a class bool False for null, a destroyed UnityEngine.Object or an empty string.
tb:percent float, double 0 to 1 Length, StyleLength 0.25 gives 25%.
tb:hex Color string #RRGGBB, or #RRGGBBAA when not opaque. Both ways; text that is not a color gives clear. Use is-delayed="true" on the text field.
tb:display-name an enum string The [InspectorName], or the name with spaces: MaxHealth gives "Max Health".
tb:f0, tb:f1, tb:f2 int, long, float, double string 0, 1 or 2 decimals.
tb:n0 int, long, float, double string Thousands separators: "1,234,567".
tb:percent-text int, long, float, double string 0.256 gives "26%".
tb:currency int, long, float, double string Money in the current culture.
tb:bytes int, long, float, double string 1536000 gives "1.5 MB" (1024 based).
tb:duration seconds (int, long, float, double), TimeSpan string 5400 gives "1:30:00"; 90 gives "1:30".
tb:date, tb:time, tb:datetime DateTime string Short date, short time, or both.
tb:upper, tb:lower string string Upper or lower case.
tb:xy, tb:xz, tb:yz Vector2, Vector3, Vector2Int, Vector3Int Vector3, Vector2 The id names the axes of the Vector3. tb:xz: (x, y) gives (x, 0, y), and (x, y, z) gives (x, z). Both ways.
tb:enum-int an enum int The int value, both ways. For RadioButtonGroup.value and DropdownField.index.

All text groups use the current culture. The groups register when the player starts and when the editor loads. Startup adds tb:enum-int and tb:display-name for each public enum, and tb:has-value for each public class, in UIToolbox.Runtime and in the assemblies that reference it. For other types, call ToolboxConverters.RegisterEnum<KeyCode>() or ToolboxConverters.RegisterHasValue<MyType>() before the binding updates.