Skip to content

Instantly share code, notes, and snippets.

@simonwittber
Last active May 16, 2025 04:23
Show Gist options
  • Save simonwittber/caa534915be4ab0aeda4fb6cc04291d3 to your computer and use it in GitHub Desktop.
Save simonwittber/caa534915be4ab0aeda4fb6cc04291d3 to your computer and use it in GitHub Desktop.
State machine for working with graph like editors.
using UnityEditor;
using UnityEngine;
public enum Interaction
{
Idle,
MouseDownOnItem,
DragItemBegin,
MouseDownOnSelectedItem,
DragItemUpdate,
DragItemEnd,
MouseUpOnSelectedItem,
MouseContextDownOnItem,
MouseContextDownOnCanvas,
MouseDownOnCanvas,
MouseDragOnCanvas
}
public interface IDecoratedItem
{
public bool IsSelected { get; set; }
public Rect Rect { get; set; }
public Vector2 DragStartPosition { get; set; }
}
public class LayoutEditorStateMachine<T> : StateMachine<Interaction> where T: class, IDecoratedItem
{
public Action Repaint = () => throw new NotImplementedException();
public Action<T[]> OnItemsUpdated = (_) => throw new NotImplementedException();
public Action ShowCanvasContextMenu = () => throw new NotImplementedException();
public Action ShowItemContextMenu = () => throw new NotImplementedException();
private Vector2 _mouseDownPosition;
private Rect _marqueeRect;
public LayoutEditorStateMachine()
{
From(Interaction.Idle, Interaction.MouseDownOnSelectedItem, MouseDown, Button0, OverSelectedItem);
From(Interaction.Idle, Interaction.MouseDownOnItem, MouseDown, Button0, OverItem);
From(Interaction.Idle, Interaction.MouseContextDownOnItem, MouseDown, Button1, OverItem);
From(Interaction.Idle, Interaction.MouseContextDownOnCanvas, MouseDown, Button1);
From(Interaction.Idle, Interaction.MouseDownOnCanvas, MouseDown, Button0);
From(Interaction.MouseDownOnSelectedItem, Interaction.DragItemBegin, MouseDidDrag, Button0);
From(Interaction.MouseDownOnItem, Interaction.DragItemBegin, MouseDidDrag);
From(Interaction.DragItemBegin, Interaction.DragItemUpdate);
From(Interaction.DragItemUpdate, Interaction.DragItemEnd, MouseUp, Button0);
From(Interaction.DragItemEnd, Interaction.Idle);
From(Interaction.MouseDownOnItem, Interaction.Idle, MouseUp);
From(Interaction.MouseDownOnSelectedItem, Interaction.MouseUpOnSelectedItem, MouseUp);
From(Interaction.MouseUpOnSelectedItem, Interaction.Idle);
From(Interaction.MouseContextDownOnItem, Interaction.Idle);
From(Interaction.MouseContextDownOnCanvas, Interaction.Idle);
From(Interaction.DragItemBegin, Interaction.Idle, MouseUp, Button0);
From(Interaction.MouseDownOnCanvas, Interaction.Idle, MouseUp, Button0);
From(Interaction.MouseDownOnCanvas, Interaction.MouseDragOnCanvas, MouseDidDrag);
From(Interaction.MouseDragOnCanvas, Interaction.Idle, MouseUp, Button0);
OnEnter(Interaction.MouseDragOnCanvas, OnBeginMouseDragOnCanvas);
OnStay(Interaction.MouseDragOnCanvas, OnInMouseDragOnCanvas);
OnExit(Interaction.MouseDragOnCanvas, OnExitMouseDragOnCanvas);
OnEnter(Interaction.MouseDownOnItem, OnMouseDownOnItem);
OnEnter(Interaction.DragItemBegin, OnBeginDragItem);
OnStay(Interaction.DragItemUpdate, OnEnterDragItemUpdate);
OnExit(Interaction.DragItemUpdate, OnExitDragItemUpdate);
OnEnter(Interaction.MouseDownOnSelectedItem, OnMouseDownOnSelectedItem);
OnEnter(Interaction.MouseUpOnSelectedItem, OnMouseUpOnSelectedItem);
OnEnter(Interaction.MouseContextDownOnItem, OnMouseContextDownOnItem);
OnExit(Interaction.MouseContextDownOnItem, OnMouseContextUpOnItem);
OnEnter(Interaction.MouseContextDownOnCanvas, OnMouseContextDownOnCanvas);
OnEnter(Interaction.MouseDownOnCanvas, OnMouseDownOnCanvas);
state = Interaction.Idle;
}
public void SetItems(IEnumerable<T> items)
{
Items.Clear();
Items.AddRange(items);
}
public void ClearItems()
{
Items.Clear();
}
public readonly List<T> Items = new();
public T[] SelectedItems => Items.Where(r => r.IsSelected).ToArray();
private bool MouseDown()
{
if (Event.current.type != EventType.MouseDown)
return false;
_mouseDownPosition = Event.current.mousePosition;
return true;
}
private bool MouseUp() => Event.current.type == EventType.MouseUp;
private bool Button0() => Event.current.button == 0;
private bool Button1() => Event.current.button == 1;
private bool OverItem() => IndexOfItemAtMouse() >= 0;
private bool OverSelectedItem()
{
var index = IndexOfItemAtMouse();
return index >= 0 && Items[index].IsSelected;
}
private bool MouseDidDrag() => Event.current.type == EventType.MouseDrag;
private int IndexOfItemAtMouse()
{
return Items.FindIndex(r => r.Rect.Contains(Event.current.mousePosition));
}
private void OnBeginMouseDragOnCanvas()
{
var e = Event.current;
if (e.type != EventType.MouseDrag)
return;
_marqueeRect = new Rect(_mouseDownPosition, Event.current.mousePosition - _mouseDownPosition);
e.Use();
Repaint?.Invoke();
}
private void OnInMouseDragOnCanvas()
{
var e = Event.current;
if (e.type == EventType.MouseDrag)
{
_marqueeRect.max = Event.current.mousePosition;
foreach (var room in Items)
{
room.IsSelected = _marqueeRect.Overlaps(room.Rect);
}
e.Use();
Repaint?.Invoke();
}
if (e.type == EventType.Repaint)
{
Handles.DrawSolidRectangleWithOutline(_marqueeRect, new Color(1, 1, 1, 0.2f), Color.black);
}
}
private void OnExitMouseDragOnCanvas()
{
_marqueeRect = new Rect();
Repaint?.Invoke();
}
private void OnMouseDownOnItem()
{
var e = Event.current;
if (e.type == EventType.MouseDown)
{
var index = IndexOfItemAtMouse();
if (index >= 0)
{
if (!e.shift)
{
foreach (var room in SelectedItems)
room.IsSelected = false;
}
Items[index].IsSelected = !Items[index].IsSelected;
e.Use();
Repaint?.Invoke();
}
}
}
private void OnBeginDragItem()
{
var e = Event.current;
if (e.type == EventType.MouseDrag)
{
// Does not work for some reason!
// Undo.RecordObject(window._blueprint, "Move Items");
foreach (var room in SelectedItems)
{
room.DragStartPosition = room.Rect.position;
}
e.Use();
Repaint?.Invoke();
}
}
private void OnEnterDragItemUpdate()
{
var e = Event.current;
if (e.type == EventType.MouseDrag)
{
var delta = Event.current.mousePosition - _mouseDownPosition;
foreach (var room in SelectedItems)
{
var rect = room.Rect;
rect.position = room.DragStartPosition + delta;
room.Rect = rect;
}
e.Use();
Repaint?.Invoke();
}
}
protected virtual void OnExitDragItemUpdate()
{
OnItemsUpdated?.Invoke(SelectedItems);
}
private void OnMouseDownOnSelectedItem()
{
var e = Event.current;
if (e.shift)
{
var index = IndexOfItemAtMouse();
if (index >= 0)
{
Items[index].IsSelected = false;
e.Use();
Repaint?.Invoke();
}
}
}
private void OnMouseUpOnSelectedItem()
{
var index = IndexOfItemAtMouse();
if (index >= 0)
{
foreach (var i in Items)
i.IsSelected = false;
Items[index].IsSelected = true;
Repaint?.Invoke();
}
}
private void OnMouseContextDownOnItem()
{
var index = IndexOfItemAtMouse();
if (Items[index].IsSelected)
{
OnMouseDownOnSelectedItem();
}
else
{
OnMouseDownOnItem();
}
}
private void OnMouseContextUpOnItem()
{
ShowItemContextMenu?.Invoke();
}
private void OnMouseContextDownOnCanvas() => ShowCanvasContextMenu?.Invoke();
private void OnMouseDownOnCanvas()
{
foreach (var room in SelectedItems)
room.IsSelected = false;
Repaint?.Invoke();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment