Editor/Inspector/ReactionInspectorWidget.cs
#if DEBUG
using Sandbox.Reactivity.Internals;
namespace Sandbox.Reactivity.Editor.Inspector;
// ReSharper disable once UnusedType.Global
[Inspector(typeof(Effect))]
[Inspector(typeof(Derived<>))]
internal sealed class ReactionInspectorWidget : InspectorWidget
{
private readonly string _displayName;
private readonly IReaction _reaction;
private readonly Layout _rootLayout;
public ReactionInspectorWidget(SerializedObject so)
: base(so)
{
_reaction = so.Targets.OfType<IReaction>().First();
_displayName = _reaction.GetType().ToRichText();
Layout = new Column
{
Children =
[
new ScrollArea(this)
{
Canvas = new Widget
{
HorizontalSizeMode = SizeMode.Flexible,
Layout = _rootLayout = new Column(),
},
},
],
};
Rebuild();
if (_reaction is Effect effect)
{
effect.OnDisposed += Rebuild;
}
}
private void TryOpenImplementationCode()
{
if (_reaction.Location is { } location)
{
if (location.LastIndexOf(':') is var separatorIndex && separatorIndex != -1)
{
CodeEditor.OpenFile(location[..separatorIndex],
int.TryParse(location[(separatorIndex + 1)..], out var line) ? line : 0);
}
else
{
CodeEditor.OpenFile(location);
}
}
}
private void OnHeaderContextMenu()
{
var menu = new ContextMenu(this);
menu.AddOption(new Option
{
Icon = "code",
Text = "Jump to implementation",
Enabled = _reaction.Location != null,
Triggered = TryOpenImplementationCode,
});
menu.OpenAtCursor();
}
public override void OnDestroyed()
{
if (_reaction is Effect effect)
{
effect.OnDisposed -= Rebuild;
}
base.OnDestroyed();
}
private void Rebuild()
{
var isDisposed = _reaction is Effect { IsDisposed: true };
var isEffectRoot = _reaction is Effect { ShouldTrackDependencies: false };
_rootLayout.Clear(true);
_rootLayout.Children =
[
new Widget
{
Enabled = !isDisposed,
TextColor = isDisposed ? Theme.TextControl.WithAlpha(0.6f) : Theme.TextControl,
MouseRightClick = OnHeaderContextMenu,
Layout = new Row
{
Alignment = TextFlag.LeftCenter,
Margin = 8,
Spacing = 12,
Children =
[
new Label
{
Text = _reaction.Icon ?? "link",
FontFamily = "Material Icons",
FontSize = "16pt",
},
new Widget
{
Layout = new Column
{
Alignment = TextFlag.Left,
Children =
[
new Label
{
Text = _reaction.Name ?? "Reaction",
FontSize = "13pt",
FontWeight = 500,
},
new Widget
{
Layout = new Row
{
Alignment = TextFlag.LeftCenter,
Spacing = 4,
Children =
[
isEffectRoot
? new Label
{
FontFamily = "Material Icons",
FontSize = "14px",
TextColor = Theme.TextLight,
Text = "anchor",
}
: null,
new Label
{
Text = _reaction.GetType().ToRichText(),
},
isDisposed
? new Label
{
Text = "(disposed)",
FontStyle = "italic",
}
: null,
],
},
},
_reaction.Location is { } location
? new Label
{
Text = location,
TextColor = Theme.Primary.Desaturate(0.3f),
TextDecoration = "underline",
Cursor = CursorShape.Finger,
MouseClick = TryOpenImplementationCode,
}
: null,
],
},
},
new Widget
{
HorizontalSizeMode = SizeMode.Flexible,
},
new IconButton("more_horiz")
{
Background = Color.Transparent,
FixedSize = 20,
IconSize = 14,
Foreground = Theme.TextLight,
OnClick = OnHeaderContextMenu,
},
],
},
},
];
if (_reaction.Parent is { })
{
_rootLayout.Children =
[
ControlSheet.CreateRow(new SerializedReactiveParentProperty(_reaction)), new Separator(4),
];
}
if (_reaction is not Effect { ShouldTrackDependencies: false })
{
ControlSheet sheet;
_rootLayout.Children =
[
new InspectorHeader
{
Icon = "hub",
Title = "Dependencies",
Enabled = !isDisposed,
IsCollapsable = false,
ContextMenu =
menu => Settings<ReactionInspectorWidget>.PopulateMenu(menu,
property => property.Where(x => x.Group == "Dependencies"),
Rebuild),
ToolTip =
$"The reactive values that were read during this {_displayName}'s last run. If any dependency changes, this {_displayName} will run again.",
AutoBuild = true,
},
new Widget
{
Layout = sheet = new ControlSheet(),
},
];
foreach (var producer in _reaction.Dependencies)
{
sheet.AddControl<ReactionDependencyControlWidget>(
new SerializedReactionDependencyProperty(producer, _reaction));
}
}
_rootLayout.AddStretchCell();
}
}
#endif