Last active
May 16, 2025 04:23
-
-
Save simonwittber/caa534915be4ab0aeda4fb6cc04291d3 to your computer and use it in GitHub Desktop.
State machine for working with graph like editors.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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