Custom editor controls in Unity - Part 2: Buttons

I think that a great place to start with custom control development is to re-create the GUI.Button and GUILayout.Button controls since these are straightforward to create and helps to establish some of the fundamentals of custom control development. Our custom button implementation will be used in the exact same way as the regular button controls:

if (MyGUI.Button("Press Me!")) {
    Debug.Log("Button was pressed!");
}

To begin we need somewhere to define our custom button control:

using UnityEditor;
using UnityEngine;

public static class MyGUI
{
}

When implementing a control it is important to understand that Unity has two modes of handling controls:

  1. Manually positioned with GUI relative coordinates.

  2. Automatically positioned using Unity’s GUI layout engine.

It is easy to add support for automatic layout if you start by implementing the manual version first. It is rather difficult to start by implementing the layout engine version first since you cannot use any of the layout engine features in the manual version.

For manual button positioning we will of course need to know the button’s position. The method signature can look something like this:

public static bool Button(Rect position, string label)
{
}

Every control must have a unique identifier which can be obtained via Unity’s API. It is a good idea to use a control hint when doing this:

private static readonly int s_ButtonHint = "MyGUI.Button".GetHashCode();

private static bool Button(Rect position, GUIContent label)
{
    int controlID = GUIUtility.GetControlID(s_ButtonHint, FocusType.Passive, position);
    bool result = false;

    ...

    return result;
}

Unity paints controls using GUIStyle instances which works in a similar way to how CSS works in web design. Styles for the current skin (dark / light skin) can be accessed via the GUI.skin property; and of course, you can construct your own custom styles if you want to. Painting should only be undertaken during a “Repaint” event:

switch (Event.current.GetTypeForControl(controlID)) {
    case EventType.Repaint:
        GUI.skin.button.Draw(position, label, controlID);
        break;
}

Event.current.GetTypeForControl retrieves the current event type from the perspective of the custom control. This is useful since Unity will filter out events that are irrelevant to that control.

Also, the controlID is provided when drawing the button style since this allows Unity to automatically determine which state of the control is to be drawn. For instance, if the control is hot then the hot styling will be used; if active then the active styling will be used; etc. The GUIStyle.Draw method also has an overload that allows you to control these things directly when necessary.

The next important feature of our homebrew button is to actually detect input events:

case EventType.MouseDown:
    // We are only interested if the GUI is currently enabled and if
    // the mouse pointer is being pressed on the button's rectangle.
    if (GUI.enabled && position.Contains(Event.current.mousePosition)) {
        // Mark button as the center of attention.
        GUIUtility.hotControl = controlID;
        // Consume event and trigger repaint of the current editor GUI.
        Event.current.Use();
    }
    break;

case EventType.MouseDrag:
    if (GUIUtility.hotControl == controlID) {
        // This helps to provide a good user experience by having the
        // button's visual state change between pressed and unpressed
        // as the mouse pointer enters or leaves the control's position.
        Event.current.Use();
    }
    break;

case EventType.MouseUp:
    if (GUIUtility.hotControl == controlID) {
        // It is very important that the control unmarks itself as the
        // center of attention otherwise the entire GUI will become
        // frozen since it still believes ur control is hot!
        GUIUtility.hotControl = 0;

        // If the mouse pointer is inside the button's position then
        // the button was clicked.
        if (position.Contains(Event.current.mousePosition)) {
            // Button clickage was detected whilst handling this event;
            // so the custom control must return `true` so that the
            // custom editor interface can respond to the click.
            result = true;

            Event.current.Use();
        }
    }
    break;

We can also respond to keyboard input; for instance, pressing the escape key to cancel interaction with the control:

case EventType.KeyDown:
    if (GUIUtility.hotControl == controlID) {
        if (Event.current.keyCode == KeyCode.Escape) {
            GUIUtility.hotControl = 0;
            Event.current.Use();
        }
    }
    break;

So by now you may be wondering why GUIContent was used for the label rather than a basic string. GUIContent provides a little more flexibility since it allows images and tooltips to be shown for the control. Unity handles these things automatically when using the GUIStyle.Draw method.

We can provide an overload that repackages a label string into a GUIContent instance. Since GUIContent was implemented as a class (rather than a struct which would have been better) we can cache and reuse a private GUIContent instance rather than allocating a new one each time a GUI event is processed:


private static readonly GUIContent s_TempContent = new GUIContent();

private static GUIContent TempContent(string text)
{
    s_TempContent.text = text;
    s_TempContent.image = null;
    s_TempContent.tooltip = null;
    return s_TempContent;
}


public static bool Button(Rect position, string label)
{
    return Button(position, TempContent(label));
}

Now that we have a working button with manual positioning we can easily add support for Unity’s UI layout engine:

public static bool Button(GUIContent label, params GUILayoutOption[] options)
{
    Rect position = GUILayoutUtility.GetRect(label, GUI.skin.button, options);
    return Button(position, label);
}

public static bool Button(string label, params GUILayoutOption[] options)
{
    return Button(TempContent(label), options);
}

If you would like to allow the styling of your custom control to be customizable then you can introduce overloads that accept a GUIStyle argument. Here is a completed version of the above button control which has been amended to accept a GUIStyle argument:

using UnityEditor;
using UnityEngine;

public static class MyGUI
{
    // Helper functionality.

    private static readonly GUIContent s_TempContent = new GUIContent();

    private static GUIContent TempContent(string text)
    {
        s_TempContent.text = text;
        s_TempContent.image = null;
        s_TempContent.tooltip = null;
        return s_TempContent;
    }


    // Button Control - Manual Version

    private static readonly int s_ButtonHint = "MyGUI.Button".GetHashCode();

    public static bool Button(Rect position, GUIContent label, GUIStyle style)
    {
        int controlID = GUIUtility.GetControlID(s_ButtonHint, FocusType.Passive, position);
        bool result = false;

        switch (Event.current.GetTypeForControl(controlID)) {
            case EventType.MouseDown:
                if (GUI.enabled && position.Contains(Event.current.mousePosition)) {
                    GUIUtility.hotControl = controlID;
                    Event.current.Use();
                }
                break;

            case EventType.MouseDrag:
                if (GUIUtility.hotControl == controlID) {
                    Event.current.Use();
                }
                break;

            case EventType.MouseUp:
                if (GUIUtility.hotControl == controlID) {
                    GUIUtility.hotControl = 0;

                    if (position.Contains(Event.current.mousePosition)) {
                        result = true;
                        Event.current.Use();
                    }
                }
                break;

            case EventType.KeyDown:
                if (GUIUtility.hotControl == controlID) {
                    if (Event.current.keyCode == KeyCode.Escape) {
                        GUIUtility.hotControl = 0;
                        Event.current.Use();
                    }
                }
                break;

            case EventType.Repaint:
                style.Draw(position, label, controlID);
                break;
        }

        return result;
    }

    public static bool Button(Rect position, GUIContent label)
    {
        return Button(position, label, GUI.skin.button);
    }

    public static bool Button(Rect position, string label, GUIStyle style)
    {
        return Button(position, TempContent(label), style);
    }

    public static bool Button(Rect position, string label)
    {
        return Button(position, label, GUI.skin.button);
    }


    // Button Control - Layout Version

    public static bool Button(GUIContent label, GUIStyle style, params GUILayoutOption[] options)
    {
        Rect position = GUILayoutUtility.GetRect(label, style, options);
        return Button(position, label, style);
    }

    public static bool Button(GUIContent label, params GUILayoutOption[] options)
    {
        return Button(label, GUI.skin.button, options);
    }

    public static bool Button(string label, GUIStyle style, params GUILayoutOption[] options)
    {
        return Button(TempContent(label), style, options);
    }

    public static bool Button(string label, params GUILayoutOption[] options)
    {
        return Button(label, GUI.skin.button, options);
    }
}

The resulting control behaves like this:

Annotated animation of the working button.

See: Part 3: Input control