Skip to main content

๐ŸชŸ Lesson 7.2: EditorWindows with UI Toolkit

A custom Inspector reshapes a panel Unity already gives you. An EditorWindow is a blank canvas โ€” a whole dockable window that you summon from a menu and fill with anything: a batch renamer, a level painter, a build dashboard, a data auditor. In this lesson you'll create one, hang it off a menu item, and build its interface with UI Toolkit, Unity's modern retained-mode UI. We'll contrast it with the classic IMGUI OnGUI approach so you know when each fits, then assemble a small working tool in pure C#.

๐ŸŽฏ Learning Objectives

By the end of this lesson, you will be able to:

  • Create an EditorWindow subclass and open it from a [MenuItem]
  • Contrast IMGUI (OnGUI, immediate mode) with UI Toolkit (CreateGUI, retained mode) and choose between them
  • Build an interface in CreateGUI() by adding VisualElements โ€” Label, Button, TextField, ListView โ€” to rootVisualElement
  • Explain when to author UI in UXML/USS versus building it in C#
  • Wire up callbacks and basic data binding

Estimated Time: 70 minutes  ยท  Prerequisite: Lesson 7.1 (custom Editors, the Editor assembly boundary)

In This Lesson

A Window from a Menu Item

Every custom window is a class that derives from EditorWindow. To make it openable, you add a static method decorated with [MenuItem] that calls GetWindow. That's the whole entry point:

using UnityEngine;
using UnityEditor;

public class LevelToolWindow : EditorWindow
{
    // Adds "Tools/Level Tool" to Unity's main menu bar.
    [MenuItem("Tools/Level Tool")]
    public static void ShowWindow()
    {
        // GetWindow finds an existing instance or creates one, and focuses it.
        var window = GetWindow<LevelToolWindow>();
        window.titleContent = new GUIContent("Level Tool");
        window.minSize = new Vector2(320, 240);
    }
}

GetWindow<T>() is deliberately idempotent: call it twice and you get the same window brought to focus, not two copies. The window is automatically dockable โ€” the user can drag it beside the Inspector or Hierarchy like any built-in panel. Setting titleContent names its tab and minSize stops it collapsing to nothing. From here, the only question is how you fill it โ€” and that's where the two UI systems diverge.

๐Ÿ’ก Menu paths are namespaced by their first segment. "Tools/Level Tool" puts your item under a top-level Tools menu. You can nest deeper ("Tools/Level/Open Painter"), add a priority number as a second argument to control ordering, and even add validation methods. Keeping your studio's tools under one Tools heading keeps the menu bar sane.

IMGUI vs. UI Toolkit

Unity has two Editor UI systems, and an EditorWindow can use either. Understanding the difference is the key decision of this lesson.

๐Ÿ” IMGUI โ€” immediate mode

You override OnGUI(), which runs every repaint. You re-issue every control each call: if (GUILayout.Button("Go")) โ€ฆ. There is no persistent UI tree โ€” the interface is rebuilt from your code dozens of times a second. Quick for tiny tools; hard to scale and to style.

๐ŸŒณ UI Toolkit โ€” retained mode

You override CreateGUI(), which runs once. You build a persistent tree of VisualElements that Unity keeps and redraws for you. It styles with USS (CSS-like), lays out with flexbox, and scales to complex tools. The modern, recommended path.

๐Ÿ“– Definition

Immediate mode (IMGUI): the UI has no lasting state; your code redraws the entire interface every frame, and a "button" is really "did a click land in this rectangle this frame?" Retained mode (UI Toolkit): the UI is a persistent object graph (like a web page's DOM); you build it once, mutate it when data changes, and the framework handles redrawing and events.

The mermaid diagram makes the shape of each explicit โ€” IMGUI's loop versus UI Toolkit's build-once tree:

flowchart LR subgraph IM["IMGUI โ€” OnGUI() runs every repaint"] direction TB A1["repaint tick"] --> A2["OnGUI(): re-issue
every control"] --> A3["draw"] --> A1 end subgraph UT["UI Toolkit โ€” CreateGUI() runs once"] direction TB B1["window opens"] --> B2["CreateGUI(): build
VisualElement tree"] --> B3["tree retained"] B3 --> B4["event / data change
mutates one element"] --> B3 end IM -->|"modern replacement"| UT

Figure 1: IMGUI rebuilds the whole UI every repaint from inside OnGUI; UI Toolkit builds a retained tree once in CreateGUI and only touches the parts that change. We teach UI Toolkit and mention IMGUI where you'll still meet it.

You still see IMGUI everywhere โ€” the custom Inspector in Lesson 7.1 used it (OnInspectorGUI is immediate mode), and plenty of small tools live happily in OnGUI. But for a real window you'll maintain, UI Toolkit wins on structure, styling, and performance. The rest of this lesson is UI Toolkit.

Building UI in CreateGUI

Override CreateGUI() instead of OnGUI(). Unity calls it once when the window opens, and hands you rootVisualElement โ€” the root of your window's UI tree. You add children to it. Everything you build hangs off that root:

using UnityEngine;
using UnityEngine.UIElements;   // VisualElement, Label, Button, TextField...
using UnityEditor;

public class HelloToolWindow : EditorWindow
{
    [MenuItem("Tools/Hello Tool")]
    public static void ShowWindow() => GetWindow<HelloToolWindow>("Hello Tool");

    // Called ONCE when the window is created. Build the tree here.
    public void CreateGUI()
    {
        VisualElement root = rootVisualElement;

        var title = new Label("My First Tool");
        title.style.fontSize = 16;
        title.style.unityFontStyleAndWeight = FontStyle.Bold;
        title.style.marginBottom = 8;
        root.Add(title);

        var nameField = new TextField("Level Name");
        root.Add(nameField);

        var button = new Button(() => Debug.Log($"Hello, {nameField.value}!"))
        {
            text = "Say Hello"
        };
        root.Add(button);
    }
}

Read that as three nouns and one verb: create a VisualElement, set some style, root.Add(...) it. The button takes an Action in its constructor โ€” the callback that runs on click โ€” and reads the text field's live .value when clicked. No repaint loop, no rectangle math; the tree persists and Unity redraws it as needed. Styling is set through .style (individual properties, like inline CSS) โ€” marginBottom, fontSize, flexbox flexDirection, and so on.

โš ๏ธ CreateGUI, not OnGUI, and not a constructor

Build UI Toolkit trees in CreateGUI(). Don't build them in a constructor (the window isn't fully initialized) and don't mix them into OnGUI() (that's the IMGUI path). If you accidentally override OnGUI, Unity will happily call it and you'll be back in immediate mode without realizing it.

The Core VisualElements

A handful of built-in controls cover most tools. Each is a class you instantiate and Add to a parent:

  • Label โ€” static or updatable text. Set .text.
  • Button โ€” takes a click Action; set .text. Also exposes a .clicked event you can subscribe to.
  • TextField โ€” editable string. Read/write .value; listen with RegisterValueChangedCallback.
  • Toggle, IntegerField, FloatField, ObjectField โ€” the typed cousins of TextField, each with a .value.
  • VisualElement โ€” a plain container; set style.flexDirection = FlexDirection.Row to lay children out horizontally, the flexbox way.
  • ListView โ€” the workhorse for showing many items efficiently (it recycles rows), which we'll use next.

ListView deserves special attention because it's how you display a collection without hand-drawing every row. You give it the backing data, a factory that makes one empty row element, and a binder that fills a row element for a given index:

// Backing data โ€” any IList works.
var items = new System.Collections.Generic.List<string>
            { "Grasslands", "Cavern", "Fortress" };

var list = new ListView
{
    itemsSource   = items,
    fixedItemHeight = 20,
    // makeItem: create ONE reusable row element (called ~ once per visible row).
    makeItem      = () => new Label(),
    // bindItem: fill a row element with the data at index i (called on scroll/refresh).
    bindItem      = (element, i) => ((Label)element).text = items[i]
};
list.style.flexGrow = 1;               // fill remaining vertical space
root.Add(list);

The makeItem/bindItem split is the important idea: ListView creates only enough row elements to fill the visible area and recycles them as you scroll, rebinding each to new data. That's why it stays fast with thousands of items โ€” the same lesson as ECS chunks and object pooling, applied to UI. Call list.Rebuild() (or list.RefreshItems()) after the backing list changes and the view catches up.

UXML/USS vs. C#-Built UI

You've been building the tree in C#. UI Toolkit offers a second route: describe the layout declaratively in UXML (an XML markup, structurally like HTML) and its styling in USS (a stylesheet, structurally like CSS), then load them into your window. The same three controls as above, in UXML:

<!-- LevelTool.uxml -->
<ui:UXML xmlns:ui="UnityEngine.UIElements">
    <ui:Label text="My First Tool" class="title" />
    <ui:TextField label="Level Name" name="nameField" />
    <ui:Button text="Say Hello" name="helloButton" />
</ui:UXML>

You load that asset and query elements by name to wire behaviour:

public void CreateGUI()
{
    // Load the UXML tree asset and clone it into our root.
    var tree = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(
                   "Assets/Editor/LevelTool.uxml");
    tree.CloneTree(rootVisualElement);

    // Find named elements and hook them up.
    var nameField = rootVisualElement.Q<TextField>("nameField");
    var button    = rootVisualElement.Q<Button>("helloButton");
    button.clicked += () => Debug.Log($"Hello, {nameField.value}!");
}

The Q<T>("name") query is the bridge โ€” it finds an element in the cloned tree by name and type, the same idea as a CSS selector. So which route do you pick?

๐Ÿ“– When to choose which

  • C#-built shines for small, dynamic tools where the layout is the logic, and for UI generated from data you don't know ahead of time. One file, no asset wrangling.
  • UXML + USS shines for larger, more visual tools: designers can edit layout and style without touching C#, styling is centralized and reusable, and Unity's UI Builder gives you a visual editor. It separates structure from behaviour the way web front-ends do.

Neither is wrong. For this module's mini-project we build in C# to keep everything in one place you can read top to bottom, but the moment a tool grows a real visual identity, UXML/USS is worth the extra files.

Callbacks & Data Binding

Two mechanisms connect your UI to data. The first is the explicit callback: subscribe to a control's change event and run code. The second is binding: point a control at a serialized property so it reads and writes that data automatically, with Undo, exactly like Lesson 7.1's Inspector fields.

// (1) Explicit callback โ€” react to a value change yourself.
var radius = new FloatField("Radius");
radius.RegisterValueChangedCallback(evt =>
    Debug.Log($"Radius changed from {evt.previousValue} to {evt.newValue}"));
root.Add(radius);

// (2) Binding โ€” tie a field to serialized data; edits flow both ways + Undo.
//     'so' is a SerializedObject wrapping some asset or component.
var speed = new FloatField("Speed") { bindingPath = "moveSpeed" };
root.Add(speed);
root.Bind(so);   // now 'speed' reflects and edits so's "moveSpeed" property

Binding is the same serialization pipeline you met in the last lesson, surfaced in UI Toolkit: set a control's bindingPath to a property name, call root.Bind(serializedObject), and the control mirrors that property โ€” showing prefab-override state, participating in Undo, and writing back on edit. Use explicit callbacks when you need to run logic on change; use binding when a control simply is a view of stored data.

A reconstructed UI Toolkit EditorWindow: the Level Tool A mock of a dockable Editor window titled "Level Tool". At the top a bold title reads "Level Layout Tool". Below it a TextField labelled "Level Name" holds the value "Forest01". Under that, a ListView shows three rows โ€” Grasslands, Cavern, Fortress โ€” with the second row highlighted as selected. At the bottom, a horizontal row of two buttons reads "Add Marker" and "Save Layout". ๐ŸชŸ Level Tool Level Layout Tool Level Name Forest01 Addressable Prefabs ๐Ÿงฉ Grasslands ๐Ÿงฉ Cavern ๐Ÿงฉ Fortress ListView recycles row elements as you scroll Add Marker Save Layout Label ยท TextField ยท ListView ยท Button โ€” all VisualElements added to rootVisualElement
Figure 2: A reconstructed UI Toolkit EditorWindow (UI Toolkit panels can't be captured cleanly, so this is drawn from the code). A Label title, a TextField, a scrolling ListView of items with one selected, and a horizontal row of two Buttons โ€” the exact controls we assemble for the Module 7 mini-project.

A Small Tool, End to End

Here is a complete, compilable window that ties the pieces together: a title, a text field, a ListView populated from a list, and a button that appends to it and refreshes the view. It is the skeleton the mini-project (Lesson 7.4) grows into.

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UIElements;
using UnityEditor;

public class LevelToolWindow : EditorWindow
{
    readonly List<string> entries = new() { "Grasslands", "Cavern", "Fortress" };
    ListView list;

    [MenuItem("Tools/Level Tool")]
    public static void ShowWindow()
    {
        var w = GetWindow<LevelToolWindow>();
        w.titleContent = new GUIContent("Level Tool");
        w.minSize = new Vector2(320, 260);
    }

    public void CreateGUI()
    {
        var root = rootVisualElement;
        root.style.paddingTop = root.style.paddingLeft =
            root.style.paddingRight = root.style.paddingBottom = 8;

        var title = new Label("Level Layout Tool");
        title.style.fontSize = 16;
        title.style.unityFontStyleAndWeight = FontStyle.Bold;
        title.style.marginBottom = 8;
        root.Add(title);

        var nameField = new TextField("Level Name") { value = "Forest01" };
        root.Add(nameField);

        list = new ListView(entries, 20,
            makeItem: () => new Label(),
            bindItem: (e, i) => ((Label)e).text = "๐Ÿงฉ " + entries[i]);
        list.selectionType = SelectionType.Single;
        list.style.flexGrow = 1;
        list.style.marginTop = 6;
        root.Add(list);

        // A horizontal button row (flexbox).
        var row = new VisualElement();
        row.style.flexDirection = FlexDirection.Row;
        row.style.marginTop = 6;

        var addBtn = new Button(() =>
        {
            entries.Add("New Region " + entries.Count);
            list.Rebuild();               // refresh the view after data changes
        }) { text = "Add Marker" };
        addBtn.style.flexGrow = 1;

        var saveBtn = new Button(() =>
            Debug.Log($"Saving '{nameField.value}' with {entries.Count} entries."))
            { text = "Save Layout" };
        saveBtn.style.flexGrow = 1;

        row.Add(addBtn);
        row.Add(saveBtn);
        root.Add(row);
    }
}

Drop that in an Editor folder, open Tools โ–ธ Level Tool, and you have the window in Figure 2 โ€” dockable, resizable, and reacting to clicks. Every piece is a VisualElement added to rootVisualElement; the button callbacks mutate the backing list and call Rebuild(). That's the entire UI Toolkit loop, and it's all you need for real tools.

โœ… It's the Editor boundary again

This whole file uses UnityEditor and EditorWindow, so โ€” exactly as in Lesson 7.1 โ€” it must live in an Editor folder or Editor-only asmdef. An EditorWindow that leaks into a runtime assembly will break your player build.

Hands-on Challenge

๐Ÿ‹๏ธ Exercise 1: A batch-rename window

Objective: Build a genuinely useful tool with the controls from this lesson.

Create a BatchRenameWindow : EditorWindow, opened from Tools โ–ธ Batch Rename, that:

  1. Has a TextField for a base name and an IntegerField for a starting index.
  2. Shows a ListView of the names of the currently selected GameObjects (Selection.gameObjects).
  3. Has a "Rename" button that renames each selected object to base + (start + i), recording Undo with Undo.RecordObject so the rename is undoable.
๐Ÿ’ก Hint

Build everything in CreateGUI. For the list, set itemsSource = Selection.gameObjects and rebuild it whenever the selection changes โ€” subscribe in OnEnable: Selection.selectionChanged += () => list.Rebuild(); and unsubscribe in OnDisable. In the Rename callback, loop the selected objects and call Undo.RecordObject(go, "Batch Rename"); go.name = baseField.value + (startField.value + i);.

โœ… Solution sketch
public class BatchRenameWindow : EditorWindow
{
    [MenuItem("Tools/Batch Rename")]
    static void Open() => GetWindow<BatchRenameWindow>("Batch Rename");

    TextField baseField;
    IntegerField startField;
    ListView list;

    public void CreateGUI()
    {
        var root = rootVisualElement;
        baseField  = new TextField("Base Name") { value = "Enemy_" };
        startField = new IntegerField("Start Index") { value = 0 };
        root.Add(baseField);
        root.Add(startField);

        list = new ListView(Selection.gameObjects, 18,
            () => new Label(),
            (e, i) => ((Label)e).text = Selection.gameObjects[i].name);
        list.style.flexGrow = 1;
        root.Add(list);

        var go = new Button(Rename) { text = "Rename" };
        root.Add(go);
    }

    void OnEnable()  => Selection.selectionChanged += Refresh;
    void OnDisable() => Selection.selectionChanged -= Refresh;
    void Refresh()   { if (list != null) { list.itemsSource = Selection.gameObjects; list.Rebuild(); } }

    void Rename()
    {
        var objs = Selection.gameObjects;
        for (int i = 0; i < objs.Length; i++)
        {
            Undo.RecordObject(objs[i], "Batch Rename");
            objs[i].name = baseField.value + (startField.value + i);
        }
    }
}

๐Ÿ‹๏ธ Exercise 2: Callback vs. binding

For each need, say whether an explicit RegisterValueChangedCallback or a bindingPath binding is the better fit: (a) a Toggle that should enable/disable a preview in the scene the instant it changes; (b) a FloatField that simply views and edits a ScriptableObject's spawnRate with Undo; (c) a TextField whose new value must be validated (rejected if it contains spaces).

โœ… Answers

(a) Callback โ€” you must run logic (toggle the preview) on change. (b) Binding โ€” it's a plain view/edit of stored data; bindingPath = "spawnRate" plus root.Bind(so) gives you two-way sync and Undo with no code. (c) Callback โ€” validation is logic; register a change callback and reject or clean the value there.

๐ŸŽฏ Quick Quiz

Question 1: How do you make an EditorWindow openable from Unity's menu bar?

Question 2: What's the core difference between IMGUI and UI Toolkit?

Question 3: In UI Toolkit, where do you build the window's interface and what do you attach it to?

Question 4: Why does ListView stay fast with thousands of items?

Summary

๐ŸŽ‰ Key Takeaways

  • An EditorWindow is a dockable custom window; open it with a static [MenuItem] method that calls GetWindow<T>().
  • IMGUI (OnGUI) is immediate mode โ€” redraw everything every repaint. UI Toolkit (CreateGUI) is retained mode โ€” build a persistent tree once. Prefer UI Toolkit.
  • In CreateGUI(), add VisualElements โ€” Label, Button, TextField, ListView โ€” to rootVisualElement; style via .style and lay out with flexbox.
  • ListView recycles row elements through makeItem/bindItem, staying fast with large collections.
  • UXML/USS describe layout and style declaratively (great for larger, visual, designer-editable tools); C#-built UI is great for small, dynamic, data-driven tools.
  • Use explicit callbacks to run logic on change; use binding (bindingPath + root.Bind) when a control just views serialized data โ€” with Undo, like Lesson 7.1.

๐Ÿš€ What's Next?

You can now build any tool window you can imagine โ€” but a level tool needs content to place, and loading a hundred prefabs the naive way blows your memory budget. In Lesson 7.3: Addressables โ€” Async Content Management, you'll learn to load assets on demand, asynchronously, and release them cleanly โ€” the system the mini-project's tool will drive.

๐ŸชŸ You build the tools now

Menu item, window, a tree of VisualElements โ€” that's the whole recipe. Every custom pipeline tool a studio relies on starts exactly here.