Fixed-width labels for fields in Unity3D Editor GUI

Why?

Unity3D’s default GUI system has dynamic label width. This ensures that the input boxes start at the same position. It can results in problems in specific cases, though. The label will be cropped if:

  • It is too long
  • The window size is too small
  • There is too much indentation before it
You can see some of the advantages and disadvantages of dynamic-width labels in the .GIF after the jump:

For the following part of the article, I am assuming you have some knowledge of C#, and Unity3D (Editor) GUI calls.

If you need any help, feel free to post a comment below. I will try to answer you or direct you to the right path when I can.

The last two labels are fixed width labels. They do not change in size, and are always fully displayed (if they fit into the window). They come with disadvantages, too: The input boxes do not start at the same x position. However, you can fake this by adding spaces before them, if you want.

How?

The issues listed above cannot be fixed with the default GUILayout.*Width() options. These options affect the whole group (label and the input box). Therefore, in order to implement fixed-width labels, you need to follow these steps:

  1. Create a new horizontal group
  2. Create a label field with a fixed width
  3. Reset indentation to zero
  4. Display all of the other fields
  5. Restore indentation
  6. Close the horizontal group
And you need to repeat the same operations for each label. Thankfully, C# allows for scoped objects, with the “using” statement and the IDisposable interface. This way, we can create a scoped “Fixed Width Label” object and dispose it after you draw the input boxes.

You can implement this by following this guide:

    1. Create a class that extends IDisposable:
      public class FixedWidthLabel : IDisposable
      {
      	//helper class to clear and restore indentation
      	private readonly ZeroIndent indentReset;
    2. In its constructor, apply the state changes:
      	public FixedWidthLabel(GUIContent label)
      	{
      		//Create a horizontal group
      		EditorGUILayout.BeginHorizontal();
      		//Display the label:
      		EditorGUILayout.LabelField(label,
      			//Fix its width:
      			GUILayout.Width(GUI.skin.label.CalcSize(label).x +
      				//Correct for previous indentation: (9 pixels per level)
      				9 * EditorGUI.indentLevel));
      		//Set following indentation to zero:
      		indentReset = new ZeroIndent();
      	}
       
      	//alternative constructor, if we don't want to deal with GUIContents
      	public FixedWidthLabel(string label) : this(new GUIContent(label))
      	{
      	}
    3. In its “Dispose” method, revert the state changes
      	public void Dispose()
      	{
      		//restore indentation state:
      		indentReset.Dispose();
      		//finish horizontal group:
      		EditorGUILayout.EndHorizontal();
      	}
      }
    4. Use it wherever you want!
      ...
      	using (new FixedWidthLabel("Fixed width (label only):"))
      		testValue = EditorGUILayout.IntField(testValue);
       
      	using (new FixedWidthLabel("This is a longer fixed width label:"))
      	{
      		testValue = EditorGUILayout.IntField(testValue);
      	}
      ...

I have omitted the declarations for the ZeroIndent class in this section, but it can be found below. It also extends the IDisposable interface, and (as you might have guessed) is used to clear the indentation to the right of the label. If we do not use the ZeroIndent class, because we are in horizontal mode, there would be 9*indentLevel amount of empty pixels after the label, which we do not want.

Of course, you can customize the code to have any amount of space you want after the label, if necessary. You can do this by changing the ZeroIndent class to have an indentation value more than 0, or by adding more to the GUILayout.Width call in the FixedWidthLabel constructor.

What?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
using System;
using UnityEditor;
using UnityEngine;
 
//FixedWidthLabel class. Extends IDisposable, so that it can be used with the "using" keyword.
public class FixedWidthLabel : IDisposable
{
	private readonly ZeroIndent indentReset; //helper class to reset and restore indentation
 
	public FixedWidthLabel(GUIContent label)//	constructor.
	{//						state changes are applied here.
		EditorGUILayout.BeginHorizontal();// create a new horizontal group
		EditorGUILayout.LabelField(label,
			GUILayout.Width(GUI.skin.label.CalcSize(label).x +// actual label width
				9 * EditorGUI.indentLevel));//indentation from the left side. It's 9 pixels per indent level
 
		indentReset = new ZeroIndent();//helper class to have no indentation after the label
	}
 
	public FixedWidthLabel(string label)
		: this(new GUIContent(label))//alternative constructor, if we don't want to deal with GUIContents
	{
	}
 
	public void Dispose() //restore GUI state
	{
		indentReset.Dispose();//restore indentation
		EditorGUILayout.EndHorizontal();//finish horizontal group
	}
}
 
class ZeroIndent : IDisposable //helper class to clear indentation
{
	private readonly int originalIndent;//the original indentation value before we change the GUI state
	public ZeroIndent()
	{
		originalIndent = EditorGUI.indentLevel;//save original indentation
		EditorGUI.indentLevel = 0;//clear indentation
	}
 
	public void Dispose()
	{
		EditorGUI.indentLevel = originalIndent;//restore original indentation
	}
}

And here is an example window which has been used to test the fixed width labels:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
using System;
using UnityEditor;
using UnityEngine;
 
public class TestWindow : EditorWindow {
	[MenuItem("Test/Test Window")]
	public static void Init()
	{
		CreateInstance().Show();
	}
 
	private Vector2 scrollPosition = new Vector2(0,0);
	private int testValue;
	public void OnGUI()
	{
		scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition);
		TestLabels();
 
		EditorGUILayout.Space();
		EditorGUILayout.LabelField("After 10 levels of indentation:");
		EditorGUILayout.Space();
 
		using (new ScopedIndent(10))
		{
			TestLabels();
		}
 
		EditorGUILayout.EndScrollView();
	}
 
	private void TestLabels()
	{
		testValue = EditorGUILayout.IntField("This is a label:", testValue);
		testValue = EditorGUILayout.IntField("This is a much longer label:", testValue);
		testValue = EditorGUILayout.IntField("Min width:", testValue, GUILayout.MinWidth(300));
		testValue = EditorGUILayout.IntField("Max width:", testValue, GUILayout.MaxWidth(300));
		EditorGUILayout.BeginHorizontal();
		EditorGUILayout.LabelField("Horizontal group:");
		testValue = EditorGUILayout.IntField(testValue);
		EditorGUILayout.EndHorizontal();
		using (new FixedWidthLabel("Fixed width (label only):"))
		{
			testValue = EditorGUILayout.IntField(testValue);
		}
		using (new FixedWidthLabel("This is a longer fixed width label:"))
		{
			testValue = EditorGUILayout.IntField(testValue);
		}
		using (new FixedWidthLabel("Two vertical input boxes:"))
		{
			EditorGUILayout.BeginVertical();
			testValue = EditorGUILayout.IntField(testValue);
			testValue = EditorGUILayout.IntField(testValue);
			EditorGUILayout.EndVertical();
		}
		using (new FixedWidthLabel("Two horizontal input boxes:"))
		{
			testValue = EditorGUILayout.IntField(testValue);
			testValue = Convert.ToInt32(EditorGUILayout.TextField(testValue + ""));
		}
		using (new FixedWidthLabel("Nesting:"))
		{
			testValue = EditorGUILayout.IntField(testValue);
			using (new FixedWidthLabel("Testing:"))
			{
				testValue = EditorGUILayout.IntField(testValue);
			}
		}
		using (new FixedWidthLabel("Vertical + nested:"))
		{
			EditorGUILayout.BeginVertical();
			testValue = EditorGUILayout.IntField(testValue);
			testValue = EditorGUILayout.IntField(testValue);
			using (new FixedWidthLabel("Nesting:"))
			{
				testValue = EditorGUILayout.IntField(testValue);
				using (new FixedWidthLabel("Testing:"))
				{
					testValue = EditorGUILayout.IntField(testValue);
				}
			}
			EditorGUILayout.EndVertical();
		}
	}
}
 
public class ScopedIndent : IDisposable
{
	private readonly int delta;
	public ScopedIndent(int delta)
	{
		this.delta = delta;
		EditorGUI.indentLevel += delta;
	}
 
	public void Dispose()
	{
		EditorGUI.indentLevel -= delta;
	}
}

How the test window should look (large gif alert!):

One thought on “Fixed-width labels for fields in Unity3D Editor GUI

Leave a Reply

Your email address will not be published. Required fields are marked *