Property States


In 3.0, we introduced the property state system, which is a way for drawers (and other things attached to properties, such as resolvers and processors) to create and expose named states that can be queried and modified from the outside. 3.0 also added the property query syntax to attribute expressions, which has a powerful synergy with the state system. Property states are contained within the State member of an InspectorProperty instance, which is of type PropertyState.

All properties have three hard-coded states by default, because they are so commonly used. These states are the Visible state, the Enabled state, and the Expanded state.

The Visible state controls the visibility of the property in question. In short, if the Visible state is false, then a call to InspectorProperty.Draw() will return almost immediately without ever drawing anything. All groups have special default behaviour related to the Visible state: if all of a group's contained properties have Visible states of false, then the group will toggle its own Visible state to also be false, thus auto-hiding itself when all of its children are hidden.

The Enabled state controls whether the GUI for the property is enabled or disabled by default. Note that individual drawers can override this, and note also that the Enabled state will never cause the GUI to switch from being disabeld to being enabled, only the other way around. Many other factors, such as being ReadOnly, can cause the GUI of a property to be disabled in a way that the Enabled state will not affect. When the Enabled state is false, a call to InspectorPropert.Draw() will set the GUI.enabled to false while that property's drawer chain is drawing.

The Expanded state controls whether or not the property is expanded in the UI or not, provided that the property's drawers make use of this state in their implementation. All included Odin drawers make use of this state to control whether a property is expanded or not, and it is advised that you also make use of this state if you write your own custom drawers, rather than use a local drawer field to control it. Unlike the Visible and the Enabled state, the Expanded state is persistent; if modified, the new value will be persisted across reloads via the PersistentContext cache.

Not all properties make use of all these states! A simple property for a string, for example, will have an Expanded state, but since none of its drawers make use of this state, changing the state will have no effect on how the string is drawn.

Let's take a look at an example of an attribute declaration that uses the state system in a simple way: an enum that controls the Expanded state of a list based on whether it has a certain flag. This example uses the property query syntax of Odin's attribute expressions feature, #(exampleList), to handily work with the InspectorProperty instance of the list member and modify its state.

// It is generally recommended to use the OnStateUpdate attribute to control the state of properties
[OnStateUpdate("@#(exampleList).State.Expanded = $value.HasFlag(ExampleEnum.UseStringList)")]
public ExampleEnum exampleEnum;

public List<string> exampleList;

[Flags]
public enum ExampleEnum
{
    None,
    UseStringList = 1 << 0,
    // ...
}

It is also possible to create custom states. Any drawer, processor or resolver that wishes to create a custom state needs to call InspectorProperty.State.Create() once to do so. Afterwards, the custom state can be accessed and modified via the InspectorProperty.State.Get() and InspectorProperty.State.Set() methods.

For example, the drawer for the TabGroup attribute exposes three custom states: CurrentTabName, CurrentTabIndex and TabCount, that allow for the querying and changing of the currently selected tab, as in the following example:

// All groups silently have "#" prepended to their path identifier to avoid naming conflicts with members.
// Thus, the "Tabs" group is accessed via the "#(#Tabs)" syntax.
[OnStateUpdate("@#(#Tabs).State.Set<int>(\"CurrentTabIndex\", $value + 1)")]
[PropertyRange(1, "@#(#Tabs).State.Get<int>(\"TabCount\")")]
public int selectedTab = 1;

[TabGroup("Tabs", "Tab 1")]
public string exampleString1;

[TabGroup("Tabs", "Tab 2")]
public string exampleString2;

[TabGroup("Tabs", "Tab 3")]
public string exampleString3;