Home

Awesome

Markup Attributes

A Unity Editor extension for customizing inspector layout with attributes.

Key Features

Why?

Anyone who wrote custom editors knows, that they are prone to boilerplate and often rely on hardcoded names of the properties, which adds unnecessary friction to development. One way to deal with it is to use C# Attributes, and the most prominent project to do so is Odin Inspector. It is great, but it can't be used in open source and on the Asset Store because it's paid and huge. Also, as far as know, it doesn't do anything for shader editors, which drag with them even more boilerplate and bookkeeping that the regular ones. So, here is my take on the problem.

Markup Attributes is MIT licensed and relatively small, focusing exclusively on editor layout. It works both in C# and in ShaderLab. Custom inspector provides hooks at any of the properties, which makes it possible to extend the inspector without loosing the layout functionality.

Table of Contents

  1. Installation
  2. Usage
  3. Layout Attributes
  4. Special Attributes
  5. Custom Marked Up Inspectors
  6. MarkupGUI

Installation

You can install MarkupAttributes with Unity Package Manager. Git URL:

https://github.com/gasgiant/Markup-Attributes.git#upm

Usage

MonoBehaviour and ScriptableObject

For MonoBehaviours and ScriptableObjects you need to create a custom editor that inherits form MarkedUpEditor:

using UnityEditor;
using MarkupAttributes;

[CustomEditor(typeof(MyComponent)), CanEditMultipleObjects]
internal class MyComponentEditor : MarkedUpEditor
{        
}

Alternatively, you can apply MarkedUpEditor to all classes that inherit from MonoBehaviour or ScriptableObject and don't have their own custom editor:

[CustomEditor(typeof(MonoBehaviour), true), CanEditMultipleObjects]
internal class MarkedUpMonoBehaviourEditor : MarkedUpEditor
{
}

[CustomEditor(typeof(ScriptableObject), true), CanEditMultipleObjects]
internal class MarkedUpScriptableObjectEditor : MarkedUpEditor
{
}

Serializable classes and structs

To make the attributes work inside serialized classes or structs you can add MarkedUpType to their definition or add MarkedUpField to the fields representing them:

[MarkedUpType]
[System.Serializable]
struct MyStruct
{
    ...
}

[System.Serializable]
class MyClass
{
    ...
}

class MyComponent : MonoBehaviour
{
    public MyStruct myStruct;

    [MarkedUpField]
    public MyClass myClass;
}

Note, that the attributes will work in marked up types only inside MarkedUpEditor. Currently marked up types are not supported inside arrays and lists. You can nest marked up types inside other marked up types.

Shaders

To apply attributes to the materials with a certain Shader you should tell Unity to use MarkedUpShaderGUI:

Shader "Unlit/MyShader"
{
    Properties
    {
        ...
    }

    CustomEditor "MarkupAttributes.Editor.MarkedUpShaderGUI"
    
    ...
}

Nesting Groups

Any group attribute requires a path in group hierarchy. The last entry in the path is the name of the group.

[Box("Group")]
public int one;
[TitleGroup("Group/Nested Group")]
public int two;
public int three;

Starting a group closes all groups untill a path match.

[Box("Group")]
public int one;
[TitleGroup("Group/Nested Group 1")]
public int two;
public int three;
[TitleGroup("Group/Nested Group 2")]
public int four;
public int five;

./ shortcut opens a group on top of the current one, ../ closes the topmost group and then opens a new one on top.

[Box("Group")]
public int one;
[TitleGroup("./Nested Group 1")]
public int two;
public int three;
[TitleGroup("../Nested Group 2")]
public int four;
public int five;

EndGroup closes the topmost group, or, when provided with a name, closes the named group and all of its children.

[Box("Group")]
public int one;
[Box("./Nested Group")]
public int two;
public int three;
[EndGroup("Group")]

public int four;
public int five;

ShaderLab Specifics

Unfortunately, ShaderLab does not allow any special symbols in property attributes. Because of that, we can't use / to write paths and have to replace them with spaces. Underscores then mark were you want actual spaces to be. Also, unlike in C#, you should not use quotes around the strings. For example, instead of

[Box("Parent Group/My Box")]

you would write

[Box(Parent_Group My_Box)].

The same goes for shortcuts, so

[Box("./My Box")] and [Box("../My Box")]

becomes

[Box(. MyBox)] and [Box(.. My_Box)].

Layout Attributes

EndGroup

Closes the topmost group. If provided with a name, closes the named group and all its children.

Box

Starts a vertical group in a box.

ParameterDescription
string pathPath to the group (see Nesting Groups).
bool labeledAdds a label to the box. Default: true.
float spaceAdds space before the group. Default: 0.
// C#
[Box("Labeled Box")]
public int one;
public int two;
public int three;

[Box("Unlabeled Box", labeled: false)]
public int four;
public int five;
public int six;
// ShaderLab
[Box(Labeled_Box)]
_One("One", Float) = 0
_Two("Two", Float) = 0
_Three("Three", Float) = 0

[Box(Unlabeled_Box, false)]
_Four("Four", Float) = 0
_Five("Five", Float) = 0
_Six("Six", Float) = 0

TitleGroup

Starts a vertical group with a title.

ParameterDescription
string pathPath to the group (see Nesting Groups).
bool contentBoxAdds a box around the group content. Default: false.
bool underlineUnderlines the title. Default: true.
float spaceAdds space before the group. Default: 3.
// C#
[TitleGroup("Title Group")]
public int one;
public int two;
public int three;

[TitleGroup("Title Group With A Content Box", contentBox: true)]
public int four;
public int five;
public int six;
// ShaderLab
[TitleGroup(Title_Group)]
_One("One", Float) = 0
_Two("Two", Float) = 0
_Three("Three", Float) = 0

[TitleGroup(Title_Group_With_A_Box, true)]
_Four("Four", Float) = 0
_Five("Five", Float) = 0
_Six("Six", Float) = 0

Foldout

Starts a collapsible vertical group.

ParameterDescription
string pathPath to the group (see Nesting Groups).
bool boxPuts the foldout inside a box. Default: true.
float spaceAdds space before the group. Default: 0.
// C#
[Foldout("Foldout In A Box")]
public int one;
public int two;
public int three;

[Foldout("Foldout", box: false)]
public int four;
public int five;
public int six;
// ShaderLab
[Foldout(Foldout_In_A_Box)]
_One("One", Float) = 0
_Two("Two", Float) = 0
_Three("Three", Float) = 0

[Foldout(Foldout, false)]
_Four("Four", Float) = 0
_Five("Five", Float) = 0
_Six("Six", Float) = 0

TabScope and Tab

TabScope creates a control for switching tabs. Tab starts a group placed on a specified page. Names of the pages must match the names defined in TabScope.

TabScope

ParameterDescription
string pathPath to the group (see Nesting Groups).
string tabsNames of the tabs separated by <code>|</code> in C# and by space in ShaderLab.
bool boxPuts the tabs inside a box. Default: false.
float spaceAdds space before the group. Default: 0.

Tab

ParameterDescription
string pathPath to the group. Name of the group must match one of the names specified in TabScope. See Nesting Groups.
// C#
[TabScope("Tab Scope", "Left|Middle|Right", box: true)]
[Tab("./Left")]
public int one;
public int two;
public int three;

[Tab("../Middle")]
public int four;
public int five;
public int six;

[Tab("../Right")]
public int seven;
public int eight;
public int nine;
// ShaderLab
[TabScope(Tab_Scope, Left Middle Right, true)]
[Tab(. Left)]
_One("One", Float) = 0
_Two("Two", Float) = 0
_Three("Three", Float) = 0

[Tab(.. Middle)]
_Four("Four", Float) = 0
_Five("Five", Float) = 0
_Six("Six", Float) = 0

[Tab(.. Right)]
_Seven("Seven", Float) = 0
_Eight("Eight", Float) = 0
_Nine("Nine", Float) = 0

HorizontalGroup and VerticalGroup

HorizontalGroup and VerticalGroup start horizontal and vertical groups, respectively.

VerticalGroup

ParameterDescription
string pathPath to the group (see Nesting Groups).
float spaceAdds space before the group. Default: 0.

HorizontalGroup

ParameterDescription
string pathPath to the group (see Nesting Groups).
float labelWidthLabel width inside the horizontal group.
float spaceAdds space before the group. Default: 0.
// C#
[HorizontalGroup("Split", labelWidth: 50)]
[VerticalGroup("./Left")]
public int one;
public int two;
public int three;

[VerticalGroup("../Right")]
public int four;
public int five;
public int six;
// ShaderLab
[HorizontalGroup(Split, 50)]
[VerticalGroup(. Left)]
_One("One", Float) = 0
_Two("Two", Float) = 0
_Three("Three", Float) = 0

[VerticalGroup(.. Right)]
_Four("Four", Float) = 0
_Five("Five", Float) = 0
_Six("Six", Float) = 0

ReadOnly

ReadOnly and ReadOnlyGroup disable a property or a group of properties in the inspector.

ParameterDescription
string pathPath to the group (see Nesting Groups).
// C#
[ReadOnly]
public int one;

[ReadOnlyGroup("Read Only Group")]
public int two;
public int three;
public int four;
// ShaderLab
[ReadOnly]
_One("One", Float) = 0

[ReadOnlyGroup(Read_Only_Group)]
_Two("Two", Float) = 0
_Three("Three", Float) = 0
_Four("Four", Float) = 0

Conditionals

HideIf, ShowIf, HideIfGroup and ShowIfGroup hide/show properties or groups of properties depending on a condition.

DisableIf, EnableIf, DisableIfGroup and EnableIfGroup disable/enable properties or groups of properties depending on a condition.

Non-Group attributes work on the property they are applied to. Group attributes work like all other groups and require a path (see Nesting Groups).

C#

HideIf, ShowIf, DisableIf, EnableIf

ParameterDescription
string memberNameMember to check the value of. Can be instance or static. Can be a field, a property or a method, that takes no arguments. If no value is provided, must be of type bool.
object value (optional)Condition will check if member equals value using Equals method.

HideIfGroup, ShowIfGroup, DisableIfGroup, EnableIfGroup

ParameterDescription
string pathPath to the group (see Nesting Groups).
string memberNameMember to check the value of. Can be instance or static. Can be a field, a property or a method, that takes no arguments. If no value is provided, must be of type bool.
object value (optional)Condition will check if member equals value using Equals method.
private bool boolField;
private bool BoolProperty => one % 2 == 0;
private static bool BoolMethod() => true;
public enum SomeEnum { Foo, Bar }
public SomeEnum state;

// Hide If Field
[HideIf(nameof(boolField))]
public int one;
[HideIf(nameof(boolField))]
public int two;

// Enable If Property
[EnableIfGroup("Enable If Property", nameof(BoolProperty))]
public int three;
public int four;
[EndGroup]

// Disable If Method
[DisableIfGroup("Disable If Method", nameof(BoolMethod))]
public int five;
public int six;
[EndGroup]

// Show If Enum Value
[ShowIf(nameof(state), SomeEnum.Foo)]
public int seven;
[ShowIf(nameof(state), SomeEnum.Bar)]
public int eight;

ShaderLab

HideIf, ShowIf, DisableIf, EnableIf

ParameterDescription
string conditionName of the condition. Can be a float property (true if greater than zero, false otherwise), material keyword, or a global keyword (to indicate that keyword is global add G before it).

HideIfGroup, ShowIfGroup, DisableIfGroup, EnableIfGroup

ParameterDescription
string pathPath to the group (see Nesting Groups).
string conditionName of the condition. Can be a float property (true if greater than zero, false otherwise), material keyword, or a global keyword (to indicate that keyword is global add G before it).
// Float Property Condition
_Toggle("Toggle", Float) = 0
[ShowIf(_Toggle)]
_One("One", Float) = 0
[ShowIf(_Toggle)]
_Two("Two", Float) = 0

// Material Keyword Condition
[EnableIfGroup(Enable_If_Keyword, MY_KEYWORD)]
_Three("Three", Float) = 0
_Four("Four", Float) = 0

// Global Keyword Condition
[DisableIfGroup(Hide_If_Global_Keyword, G MY_KEYWORD)]
_Five("Five", Float) = 0
_Six("Six", Float) = 0

ToggleGroup

Starts a vertical group with a toggle, that can be hidden or disabled depending on the toggle value.

C#

In C# ToggleGroup should be used on a serialized bool field.

ParameterDescription
string pathPath to the group (see Nesting Groups).
bool foldableMakes the group collapsible. Default: false.
bool boxPuts the group inside a box. Default: true.
float spaceAdds space before the group. Default: 0.
[ToggleGroup("Toggle Group")]
public bool boolean;
public int one;
public int two;
public int three;

[ToggleGroup("Foldable Toggle Group", foldable: true)]
public bool anotherBoolean;
public int four;
public int five;
public int six;

ShaderLab

In ShaderLab ToggleGroup should be used on a float property.

ParameterDescription
string pathPath to the group (see Nesting Groups).
bool foldableMakes the group collapsible. Default: false.
bool boxPuts the group inside a box. Default: true.
string keyword (optional)Keyword to turn on and off (like the built-in Toggle drawer).
[ToggleGroup(Toggle_Group)]
_Toggle("Toggle", Float) = 0
_One("One", Float) = 0
_Two("Two", Float) = 0
_Three("Three", Float) = 0

[ToggleGroup(Toggle_Group_With_Keyword, true, MY_KEYWORD)]
_AnotherToggle("Another Toggle", Float) = 0
_Four("Four", Float) = 0
_Five("Five", Float) = 0
_Six("Six", Float) = 0

Special Attributes

MarkedUpType

C# only

Makes attributes work inside serializable classes and structs. See Usage: Serializable classes and structs. Can optionally hide the target's control (foldout) and remove indent from target's children.

[MarkedUpType]
class SomeClass
{
    ...
}

[MarkedUpType(indentChildren: false)]
struct SomeStruct
{
    ...
}

MarkedUpField

C# only

Makes attributes work inside fields of serializable classes and structs. See Usage: Serializable classes and structs. Can optionally hide the target's control (foldout) and remove indent from target's children.

[MarkedUpField]
public SomeClass one;

[MarkedUpField(indentChildren: false)]
public SomeClass two;

[MarkedUpField(indentChildren: false, showControl: false)]
public SomeClass three;

InlineEditor

C# only

Shows the inspector of some Unity.Object (MonoBehaviour, ScripatableObject and Material are Unity.Objects, for instance) "inline" — embeds it in the current inspector. Can be used on an object reference field. Works in MarkedUpEditors and MarkedUpFields inside them.

ParameterDescription
InlineEditorMode modeBoxShows object field and inspector body inside a foldable box.
ContentBoxShows object field with foldable inspector body.
StrippedShows only inspector body.
[InlineEditor]
public SomeData someData;

[InlineEditor(InlineEditorMode.ContentBox)]
public SomeComponent someComponent;

[TitleGroup("Stripped")]
[InlineEditor(InlineEditorMode.Stripped)]
public SomeData stripped1;

DrawSystemProperties

ShaderLab only

Tells MarkedUpShaderGUI to draw Render Queue, Enable Instancing (if applicable) and Double Sided Global Illumination properties below this property.

[DrawSystemProperties]
_SomeProperty("Some Property", Float) = 0

Custom Marked Up Inspectors

Regular Inspectors

MarkedUpEditor allows to inject custom code into itself before, after and instead of any property. Here are the methods used for extension:

using UnityEditor;
using UnityEngine;
using MarkupAttributes.Editor;

[CustomEditor(typeof(MyComponent))]
public class MyComponentEditor : MarkedUpEditor
{
    protected override void OnInitialize()
    {
        AddCallback(serializedObject.FindProperty("one"), 
            CallbackEvent.AfterProperty, ButtonAfterOne);

        AddCallback(serializedObject.FindProperty("six"),
            CallbackEvent.BeforeProperty, ButtonBeforeSix);

        AddCallback(serializedObject.FindProperty("three"),
            CallbackEvent.ReplaceProperty, ButtonReplaceThree);
    }

    private void ButtonAfterOne(SerializedProperty property)
    {
        GUILayout.Button("After One");
    }

    private void ButtonBeforeSix(SerializedProperty property)
    {
        GUILayout.Button("Before Six");
    }

    private void ButtonReplaceThree(SerializedProperty property)
    {
        GUILayout.Button("Replace Three");
    }
}

Material Inspectors

MarkedUpShaderGUI can be extended in a similar manner. It provides the following methods:

using UnityEditor;
using UnityEngine;
using MarkupAttributes.Editor;

public class MyShaderEditor : MarkedUpShaderGUI
{
    protected override void OnInitialize(MaterialEditor materialEditor, MaterialProperty[] properties)
    {
        AddCallback(FindProperty("_Color", properties), CallbackEvent.AfterProperty, ButtonAfterColor);
    }

    private void ButtonAfterColor(MaterialEditor materialEditor, MaterialProperty[] properties,
        MaterialProperty property)
    {
        GUILayout.Button("Button After Color");
    }
}

Don't forget to tell Unity to use the modified editor for your shader.

Shader "Unlit/MyShader"
{
    Properties
    {
        ...
    }

    CustomEditor "MyShaderEditor"
    
    ...
}

MarkupGUI

Class MarkupGUI exposes methods for creating Markup Attributes styled groups. They can be useful for making EditorWindows or custom inspectors, that don't use attributes themselves.

using UnityEditor;
using MarkupAttributes.Editor;

public class CustomWindow : EditorWindow
{
    MarkupGUI.GroupsStack groupsStack = new MarkupGUI.GroupsStack();
    int activeTab = 0;
    bool foldout = true;

    public static void Open()
    {
        CustomWindow window = (CustomWindow)GetWindow(typeof(CustomWindow), 
            false, "Custom Window Sample");
        window.Show();
    }

    void OnGUI()
    {
        // Always clear the groups stack.
        groupsStack.Clear();

        groupsStack += MarkupGUI.BeginBoxGroup("Box");
        // group contents ...
        groupsStack.EndGroup();

        groupsStack += MarkupGUI.BeginTitleGroup("TitleGroup", true);
        // group contents ...
        groupsStack.EndGroup();

        groupsStack += MarkupGUI.BeginFoldoutGroup(ref foldout, "Foldout");
        // group contents ...
        groupsStack.EndGroup();

        groupsStack += MarkupGUI.BeginTabsGroup(ref activeTab, 
            new string[] { "Left", "Middle", "Right" }, true);
        // group contents ...

        // Always end all groups at the end.
        groupsStack.EndAll();
    }
}