Installation
- Add to the VRChat Creator Companion using this link: Add to the VCC
- Make sure the "orels-Layout-Toolkit" Repository is added and selected in the Settings screen
- Open your World project and add the "ORL Layout Toolkit" Package
Single-file Installation
A single-file version of the package is available for download here.
- You can drop
ORLLayoutToolkit.csfile into any Editor folder in your project and it should work as expected - The only limitation of this approach is that you will not get the utility classes stylesheet included, so things like
Label().Class("pr-2")will need to be written likeLabel().Padding(0, 4, 0, 0) - If you're looking to actively edit the source code - it is recommended to use single-file installations, that will allow you to extend all the included partial classes easily
Setup
- Inherit your EditorWindows from
ORL.Layout.EnhancedEditorWindowand your custom editors fromORL.Layout.EnhancedEditor - Override the
protected VisualElement Render()method - See examples below
Simple Editor Window
using ORL.Layout;
using ORL.Layout.Extensions;
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;
namespace Examples
{
public class SimpleEditorWindow : EnhancedEditorWindow
{
[MenuItem("Tools/Simple Editor Window")]
public static void ShowWindow()
{
var window = GetWindow<SimpleEditorWindow>();
window.titleContent = new GUIContent("Simple Editor Window");
window.Show();
}
private readonly ReactiveProperty<int> _counter = new(0);
protected override VisualElement Render()
{
return VStack(
Label("Sophisticated Couter").Bold().Margin(0, 0, 4),
HStack(
Label().BoundPropText(_counter, c => $"Counter: {c}"),
Button(() => _counter.Set(_counter + 1)).Text("Increment")
).AlignItems(Align.Center).JustifyContent(Justify.SpaceBetween)
).Padding(4);
}
}
}
Let's go over the code a little bit
- We defined a
Render()method that returns aVisualElementtree - We started with a VStack (vertical stack) which is usually a good starting point for any layout as it simply aligns any children vertically
- We added a
Labelelement which was then bound to aReactiveProperty- A
ReactivePropertyis a simple container for any value, which can then be observed viaOnChangeand updated withSetmethods - In case of a
Labelif we had astringproperty - we could've simply bound it to theLabelwithBoundPropText(_myStringProp)method, as it is directly compatible withstringtype - Since we're using
ReactiveProperty<int>in this case - you need to supply a compute method, in this casec => $"Counter: {c}"which will be called every time the value changes
- A
- We then created a button that calls
Setmethod on theReactivePropertyto increment the counter, which automatically updates theLabeltext - The rest of the calls like
Bold(),Margin(),Padding()are all utility layout methods to make the inspector looks nicer
The final result looks something like this

Simple Custom Inspector
Let's make a simple MonoBehaviour to hold some example data first
using UnityEngine;
namespace Examples
{
public class Enemy : MonoBehaviour
{
public float health = 100;
public float damage = 10;
public Transform spawnPoint;
public Transform target;
}
}
This should be a good starting point.
By default, such a behaviour will have an inspector like this

Which can be good enough for a lot of things, but what if you wanted some quick buttons to reset the health, or to respawn the enemy?
Let's write an inspector for it to enhance the base unity editing functionality
using ORL.Layout;
using ORL.Layout.Extensions;
using UnityEditor;
using UnityEngine.UIElements;
namespace Examples
{
[CustomEditor(typeof(Enemy))]
public class EnemyEditor : EnhancedEditor
{
protected override VisualElement Render()
{
return VStack(
HStack(
PropertyField(serializedObject.FindProperty("health")).Flex(1),
Button(() =>
{
serializedObject.FindProperty("health").floatValue = 100;
serializedObject.ApplyModifiedProperties();
}).Text("Reset")
).JustifyContent(Justify.SpaceBetween),
PropertyField(serializedObject.FindProperty("damage")),
Label("Positions").Bold().Margin(10, 0, 0, 3),
PropertyField(serializedObject.FindProperty("spawnPoint")),
PropertyField(serializedObject.FindProperty("target")),
Foldout().Text("Utilities").Class("unity-foldout").ViewDataKey("utilities-foldout").Margin(0, -4, 0, -14)
.Children(
HStack(
Button(() =>
{
var enemy = (Enemy)target;
enemy.health = 100;
enemy.transform.position = enemy.spawnPoint.position;
}).Text("Respawn"),
Button(() =>
{
var enemy = (Enemy)target;
enemy.health = 0;
}).Text("Kill")
)
)
);
}
}
}
Let's walk through the new things in this example
- Since this is a custom inspector - it is common to rely on
serializedObjectto access the properties of the target object, so we usePropetyFieldfor a lot of regular fields as there is no reason to reinvent the wheel - To adjust the
healthproperty field via a "Reset" button we simply referenced it through the serialized object and updated all the properties in-place.- You can also use
ReactiveProperty<float>in a case like this, and then bind it to aserializedPropertyso it would automatically get updated when you callSet. Here's how that would look
- You can also use
private readonly ReactiveProperty<float> _health = new(100);
protected override VisualElement Render()
{
var healhProp = serializedObject.FindProperty("health");
_health.BindToProperty(healhProp).Set(healhProp.floatValue);
return VStack(
PropertyField(healhProp),
Button(() => _health.Set(100)).Text("Reset")
);
}
- There is no functional difference between the two approaches, so feel free to use whichever feels the most intuitive to you
BindToPropertydoes its best to map the type you used in theReactivePropertyto the type of theserializedPropertyand will show a warning message if they are incompatible
- We also leveraged
Foldoutto create a collapsible section for the utility buttons- The
ViewDataKeyis used to persist the state of the foldout between the inspector reloads
- The
- Lastly, we interacted directly with the current target object without persisting the changes to the serialized object, which is a common pattern when you want to perform some quick actions during play mode
The final inspector ends up looking like this

That's it for a general overview! If you want to learn about all the methods and elements available - check out the full docs
If you ever encounter any issues - hop by the Discord and ask for help!