The Editor State Framework is a custom EditorWindow
abstraction designed to simplify state management and re-rendering within your custom editor, while trying to mimic a loosely react-ish base approach.
It is designed with code-first in mind, as that is my preferred approach in UITK, but is perfectly usable in conjunction with UXML through queries.
It comes with a generic OnChange
value binding method for VisualElements, which works for all input elements.
It is worthy to note, that this is by no means a professional-level, maintained, general-purpose framework, but rather a tool born out of laziness and necessity in a rather specific use case.
I have some plans for quality of life features or general feature extensions and intentionally left this open for extension.
Hence there are no guarantees made: Side effects are to be expected, if used outside of the scope of custom utility editors and issues will likely remain unresolved if they do not tangent my editor scripting experience.
The StatefulEditorWindow
is an abstraction layer over Unity's EditorWindow
. It hides away a lot of the boilerplate coming with creating editor windows and provides a concise number of semantically sound and intuitive base methods to facilitate a more efficient and straight-forward implementation process.
It establishes a state-driven rendering paradigm, where UI logic is encapsulated within the render method. Re-renders are triggered by changes to stateful variables, to create a straightforward development flow.
It is also called StateHost
, as the core driving 'engine' behind this framework.
A stateful variable of generic type, comparable to useState
in a single field. Modifying it will invoke a re-render.
- Open your unity package manager (
Window > Package Manager
) - Click
+ > Add package from git url
- Enter
https://github.com/AbandonedCrypt/editor-state.git
- You're Done
- Create a new Editor
- Inherit from
StatefulEditorWindow
- Remove everything but the decorated static
Show()
method. - Implement the abstract methods
Init()
andRender()
- Add stateful variables
public class MyEditor : StatefulEditorWindow
{
// state variables
private StateVar<bool> flag;
private StateVar<DisplayMode> displayMode;
[MenuItem("Your/Custom/Editor")]
public static void ShowEditor()
{
MyEditor wnd = GetWindow<MyEditor>();
wnd.titleContent = new GUIContent("My Editor");
wnd.minSize = new Vector2(250f, 360f);
}
protected override void Init()
{
// StateVar must be assigned in Init()!
flag = new StateVar<bool>(this, false);
displayMode = new StateVar<DisplayMode>(this, DisplayMode.Edit);
// initialize anything else here
}
protected override void Render()
{
// the root visual element is available as root or rootVisualElement
rootVisualElement.Add(new Label("Top Label"));
root.Add(new Label("Bottom Label"));
// modifying a StateVar will re-render your editor
if(flag) //StateVars implicitly convert to their assigned type
root.Add(new Label("I will be visible when 'flag' is assigned 'true'"));
}
}
The system now supports automatic state update batching by default. State modifications no longer trigger immediate re-renders; instead, they are grouped together, combining consecutive state changes into a single re-render. This aims to simplify the update workflow by automatically supporting non-continuous but consecutive updates and reducing unnecessary re-renders.
If need be, manual state batching can be enabled by opting out of automatic state batching. Just set the protected variable useAutomaticBatching
to false
in the Init()
method and manually batch consecutive state updates into one re-render, but comes with the drawback, that non-continuous but consecutive state update logic is now required to be moved into the callback.
BatchStateUpdates(() => {
flag.Set(true);
displayMode.Set(DisplayMode.Display);
}))
In order to avoid having to pass down state vars by "prop-drilling" in increasingly bloated constructors or method parameters, or breaking your encapsulation by having to make StateVar
public to access them from children, StateVar
can now be initialized as Repository State Variables. That means they will automatically be added to the StateRepository
of their state host, but continue to work as expected. You create a Repository State Variable by giving it a name
at instantiation.
private StateVar<float> _someFloat = new(parent, .1f, "SomeFloat");
After which it will be available in the parents' state repository for retrieval in any child.
StateVar<float> someFloat = parent.StateRepository.Retrieve<float>("SomeFloat");
This way you will now only need to pass down either the State Host (StatefulEditorWindow, as IStateHost
e.g.) or the StateRepository
itself.
Each StatefulEditorWindow has a dedicated state repository, to which all of its associated StateVars will be added, as long as they are repository state vars.
You can now use a static service locator StateContext
to retrieve a state repository instance from the parent without having to pass down any instance of IStateHost
or StateRepository
You can find your editor's StateRepository by querying the StateContext
for the name of your editor from any child component.
// assume your editor is called MyEditor
var stateRepository = StateContext.Find("MyEditor");
// or more future-proof
var stateRepository = StateContext.Find(typeof(MyEditor).Name);
// retrieve your StateVar from the repository
var someFloat = stateRepository.Retrieve<float>("SomeFloat");
Disclaimer:
By nature of the solution, stale references to StateVars will not reliably be garbage collected on time. So a retrieve-call by name to a stale StateVar, from an inactive sub-host, might still return a (null-) reference, if not overwritten by a re-render.
If you create a StateVar with an identical name to another, at a later point in the hierarchy, the latter will overwrite the former in the state repository, so make sure you choose unique names.
Yes, there is no access control, so you could use this to re-render / modify any editor window from any other editor window. Should you? No idea. Is it kind of cool? I guess so.
Passing a state host around to e.g. create StateVar
at lower hierarchy levels¹ is cumbersome. That's why you can just use the StateHostLocator
to find a state host at any level in your code.
State host registration to the locator is automatically managed by your StatefulEditorWindow
.
// Find by string name of your editor
var stateHost = StateHostLocator.Find("MyEditor");
// or more future proof
var stateHost = StateHostLocator.Find(typeof(MyEditor).Name);
// use your state host however you want then
var newStatefulFloat = new StateVar<float>(stateHost, .5f);
¹ Disclaimer: Lower child hierarchy level StateVar
are not functionally supported yet. They will be overwritten and recreated at rerender. It's on the board with high priority, though.
A generic extension method for VisualElements
is provided, that offers a declarative OnChange
method on all derived elements, specifically useful for any type of input field.
// text field
var input = new TextField();
input.value = someObject.Name;
input.OnChange(value => someObject.Name = value);
// color field
var color = new ColorField();
color.value = someObject.ErrorColor;
color.OnChange(value => someObject.ErrorColor = value);
- The
uxmlSource
instance field allows you to specify the path to auxml
file, serving as the new root for the editor's element tree (Experimental) - Concrete plans to abstract away more boilerplate from the window creation exist, and will likely be realized soon™.