Custom editor controls in Unity - Part 3: Input control

More often than not editor controls will have some sort of value that is shown and editable visually. In this post I am going to walk you through the creation of a custom control that allows some flags to be toggled using a simple grid.

Here is what the finished control will look like:

Screenshot of the finished control

A lot of people don’t realize that you can omit the prefix label from controls by providing GUIContent.none for the label argument:

Screenshot of finished control without label

When there are mixed values the control is rendered with grayed out cells (relevant when working with Unity’s SerializedProperty API):

Screenshot of finished control with mixed values

Controls that allow the user to modify an input value tend to use the following pattern:

this.inputValue = SomeGUI.SomeInputControl(this.inputValue);

The Unity editor API provides convenience functions to monitor controls for changes rather than having to compare old and new values:

EditorGUI.BeginChangeCheck();
this.inputValue = SomeGUI.SomeInputControl(this.inputValue);
if (EditorGUI.EndChangeCheck()) {
    Debug.Log("The input value was changed!");
}

To keep things simple our custom control will encode multiple flags into an integer variable using bitwise operations. I have defined several helper functions to aid with this a little although their implementation is not the focus of this post:

private static int GetFlagMask(int flagIndex)
{
    return 1 << flagIndex;
}

private static bool GetFlagState(int value, int flagIndex)
{
    return (value & GetFlagMask(flagIndex)) != 0;
}

private static int ModifyFlagState(int value, int flagIndex, bool on)
{
    return on
        ? value & ~GetFlagMask(flagIndex)
        : value | GetFlagMask(flagIndex);
}

In the same way as before (with the button control) we will begin by creating the manual version of the control.

public static int FlagToggleGrid(Rect position, GUIContent label, int value, int flagCount)
{
    flagCount = Mathf.Min(32, flagCount);

    int controlID = GUIUtility.GetControlID(s_FlagToggleGridHint, FocusType.Passive, position);

    // Should a prefix label be shown before the control?
    if (label != GUIContent.none) {
        position = EditorGUI.PrefixLabel(position, controlID, label);
    }

    float cellWidth = Mathf.Floor(position.width / flagCount);

    switch (Event.current.GetTypeForControl(controlID)) {
        case EventType.MouseDown:
            if (GUI.enabled && position.Contains(Event.current.mousePosition)) {
                // Calculate zero-based index of the flag's column.
                float controlRelativeMouseX = Event.current.mousePosition.x - position.x;
                int flagIndex = Mathf.FloorToInt(controlRelativeMouseX / cellWidth);

                // Modify the value by toggling the flag.
                value = ModifyFlagState(value, flagIndex, !GetFlagState(value, flagIndex));
                // Indicate that the value was changed using the Unity API.
                GUI.changed = true;

                Event.current.Use();
            }
            break;

        case EventType.Repaint:
            Rect cellPosition = new Rect(position.x, position.y + 2, cellWidth - 1, position.height - 4);
            Color restoreColor = GUI.color;

            for (int i = 0; i < flagCount; ++i) {
                // We can check to see whether the control has been marked
                // as having mixed values. This is applicable when editing
                // the same property on multiple objects at the same time
                // since the value might be different on both objects.
                if (EditorGUI.showMixedValue) {
                    // Show all cells in grey when there are differing values.
                    GUI.color = GetFlagState(value, i) ? new Color(0f, 0.5f, 0f) : Color.grey;
                }
                else {
                    // Show set flags with green and unset flags with black.
                    GUI.color = GetFlagState(value, i) ? Color.green : Color.black;
                }

                GUI.skin.box.Draw(cellPosition, GUIContent.none, controlID);

                // Advance to position of the next cell.
                cellPosition.x = cellPosition.xMax + 1;
            }

            GUI.color = restoreColor;
            break;
    }

    return value;
}

We can then create an overload to accept a string label:

public static int FlagToggleGrid(Rect position, string label, int value, int flagCount)
{
    return FlagToggleGrid(position, TempContent(label), value, flagCount);
}

Some of Unity’s built-in controls include an overload for working with values via the SerializedProperty API. It is straightforward to include similar support for custom controls since Unity provides API’s to assist with this:

public static void FlagToggleGrid(Rect position, SerializedProperty property, GUIContent label, int flagCount)
{
    EditorGUI.BeginProperty(position, label, property);

    EditorGUI.BeginChangeCheck();
    int newValue = FlagToggleGrid(position, label, property.intValue, flagCount);
    if (EditorGUI.EndChangeCheck()) {
        property.intValue = newValue;
    }

    EditorGUI.EndProperty();
}

public static void FlagToggleGrid(Rect position, SerializedProperty property, string label, int flagCount)
{
    FlagToggleGrid(position, property, TempContent(label), flagCount);
}

And then finally we can add the layout implementations of the control functions:

public static int FlagToggleGrid(GUIContent label, int value, int flagCount, params GUILayoutOption[] options)
{
    Rect position = GUILayoutUtility.GetRect(0, 42, options);
    return FlagToggleGrid(position, label, value, flagCount);
}

public static int FlagToggleGrid(string label, int value, int flagCount, params GUILayoutOption[] options)
{
    return FlagToggleGrid(TempContent(label), value, flagCount, options);
}

public static void FlagToggleGrid(SerializedProperty property, GUIContent label, int flagCount, params GUILayoutOption[] options)
{
    Rect position = GUILayoutUtility.GetRect(0, 42, options);
    FlagToggleGrid(position, property, label, flagCount);
}

public static void FlagToggleGrid(SerializedProperty property, string label, int flagCount, params GUILayoutOption[] options)
{
    FlagToggleGrid(property, TempContent(label), flagCount, options);
}

See: Part 4: Property drawers