Editor UI tree node for debugger effects. Wraps an Effect, listens for child creation, reruns and disposal, paints the node with highlight animations and integrates with the editor context menu and inspector.
#if DEBUG
using System.IO;
using Sandbox.Reactivity.Internals;
namespace Sandbox.Reactivity.Editor.Debugger;
internal class EffectTreeNode : TreeNode<Effect>, IDisposable
{
private const float FadeDurationSeconds = 0.5f;
private readonly Effect _effect;
private readonly TimeSince _timeSinceCreated = DebuggerWidget.HighlightNewEffects ? 0 : double.MaxValue;
private TimeSince _timeSinceLastRun = double.MaxValue;
public EffectTreeNode(Effect effect)
: base(effect)
{
EditorEvent.Unregister(this);
_effect = effect;
effect.OnChildEffectCreated += OnChildEffectCreated;
effect.OnRerun += OnEffectRerun;
effect.OnDisposed += Dispose;
}
// hack since treeview items are only updated every 100 ms, enables/disables the EditorEvent.Frame method below
private bool IsAnimating
{
set
{
if (value == field)
{
return;
}
if (value)
{
EditorEvent.Register(this);
}
else
{
EditorEvent.Unregister(this);
}
field = value;
}
}
public void Dispose()
{
_effect.OnChildEffectCreated -= OnChildEffectCreated;
_effect.OnRerun -= OnEffectRerun;
_effect.OnDisposed -= Dispose;
IsAnimating = false;
if (Parent != null)
{
Parent.RemoveItem(this);
}
else if (TreeView != null)
{
// calling RemoveItem internally calls ResolveObject when removing it from its internal list which ends up
// trying to remove the effect from the tree instead of this node. setting Value to null will make
// ResolveObject default to the node itself, making it correctly remove from the tree
Value = null!;
TreeView.RemoveItem(this);
Value = _effect;
}
}
public override bool OnContextMenu()
{
var menu = new ContextMenu(TreeView);
menu.AddOption(new Option
{
Text = "Find containing object in scene",
Icon = "search",
Enabled = _effect.Parent is Component { IsValid: true } or GameObject { IsValid: true },
Triggered = () =>
{
switch (_effect.Parent)
{
case Component component:
EditorUtility.FindInScene(component);
break;
case GameObject go:
EditorUtility.FindInScene(go);
break;
}
},
});
menu.OpenAtCursor();
return false;
}
private void OnChildEffectCreated(Effect effect)
{
var node = new EffectTreeNode(effect);
AddItem(node);
TreeView?.Open(this, true);
}
private void OnEffectRerun()
{
if (!DebuggerWidget.HighlightEffectReruns)
{
return;
}
_timeSinceLastRun = 0;
Dirty();
}
// ReSharper disable once UnusedMember.Local
[EditorEvent.Frame]
private void OnAnimate()
{
RebuildOnDirty();
}
public override void OnSelectionChanged(bool selected)
{
if (selected)
{
EditorUtility.InspectorObject = _effect;
}
else if (EditorUtility.InspectorObject == _effect)
{
EditorUtility.InspectorObject = null;
}
}
public override void OnPaint(VirtualWidget item)
{
Paint.ClearPen();
Paint.ClearBrush();
Paint.SetDefaultFont();
var rect = item.Rect;
var fullRect = rect with
{
Left = 0,
Right = TreeView?.Width ?? 0f,
};
var isEven = item.Row % 2 == 0;
var textColor = Theme.TextControl;
var backgroundColor = Color.Transparent;
if (item.Selected)
{
backgroundColor = Theme.SelectedBackground;
}
else if (item.Hovered)
{
backgroundColor = Theme.SelectedBackground.WithAlpha(0.25f);
}
else if (isEven)
{
backgroundColor = Theme.SurfaceLightBackground.WithAlpha(0.1f);
}
if (_timeSinceCreated < FadeDurationSeconds)
{
var fraction = FadeDurationSeconds - _timeSinceCreated;
IsAnimating = true;
textColor = textColor.LerpTo(Color.Green, fraction);
backgroundColor = backgroundColor.LerpTo(Color.Green.WithAlpha(0.6f), fraction);
}
else if (_timeSinceLastRun < FadeDurationSeconds)
{
var fraction = FadeDurationSeconds - _timeSinceLastRun;
IsAnimating = true;
textColor = textColor.LerpTo(Color.Orange, fraction);
backgroundColor = backgroundColor.LerpTo(Color.Orange.WithAlpha(0.6f), fraction);
}
else
{
IsAnimating = false;
}
Paint.SetBrush(backgroundColor);
Paint.DrawRect(fullRect);
Paint.Pen = textColor.WithAlpha(0.6f);
rect.Left += 4;
rect.Left += Paint.DrawIcon(rect, _effect.Icon ?? "link", 16, TextFlag.LeftCenter).Width + 4;
Paint.Pen = textColor;
rect.Left += Paint.DrawText(rect, _effect.Name ?? "Effect", TextFlag.LeftCenter).Width + 4;
if (_effect.Location is { } location && DebuggerWidget.ShowSourcePath)
{
rect.Right -= 4;
var filename = Path.GetFileName(location);
var text = Paint.GetElidedText(filename, rect.Width, ElideMode.Left, TextFlag.RightCenter);
Paint.Pen = textColor.WithAlpha(0.4f);
Paint.DrawText(rect, text, TextFlag.RightCenter);
}
}
}
#endif