ReactivePanelComponent.cs

A reactive PanelComponent for s&box that integrates the platform's reactive system with UI panel components. It manages effect roots for component lifecycle, exposes reactive property container producers, provides event sending hooks to the owning GameObject, and exposes a PanelRoot scope for creating render effects.

Native Interop
#if SANDBOX
using System.Diagnostics;
using Sandbox.Reactivity.Internals;
using static Sandbox.Reactivity.Reactive;
#if JETBRAINS_ANNOTATIONS
#endif

namespace Sandbox.Reactivity;

/// <summary>
/// The reactive counterpart to <see cref="PanelComponent" /> that allows usage of reactive properties.
/// </summary>
/// <remarks>
/// Make sure you set up an effect root using <see cref="PanelRoot" /> at the top of your razor markup:
/// <code>
/// @{ using var _ = PanelRoot(); }
/// </code>
/// Engine limitations prevent this from being done automatically.
/// </remarks>
#if JETBRAINS_ANNOTATIONS
#endif
public class ReactivePanelComponent : PanelComponent, IReactivePropertyContainer, IReactivePanel
{
	private Effect? _effectRoot;

	private Effect? _renderEffectRoot;

	private int _version;

	Effect? IReactivePanel.RenderEffectRoot
	{
		get => _renderEffectRoot;
		set => _renderEffectRoot = value;
	}

	int IReactivePanel.Version
	{
		get => _version;
		set => _version = value;
	}

	Dictionary<int, IProducer> IReactivePropertyContainer.Producers { get; } = [];

	/// <inheritdoc cref="GameObjectExtensions.SendDirect" />
	public void SendDirect<T>(T eventData)
	{
		GameObject?.SendDirect(eventData);
	}

	/// <inheritdoc cref="GameObjectExtensions.SendUp" />
	public void SendUp<T>(T eventData)
	{
		GameObject?.SendUp(eventData);
	}

	/// <inheritdoc cref="GameObjectExtensions.SendDown" />
	public void SendDown<T>(T eventData)
	{
		GameObject?.SendDown(eventData);
	}

	/// <inheritdoc cref="ReactiveComponent.OnEvent" />
	public void OnEvent<T>(Func<T, bool> callback)
	{
		if (GameObject is not { } go)
		{
			return;
		}

		var manager = GameObjectEventManager.GetOrCreate(go);

		Effect(() =>
		{
			manager.Add(callback);

			return () => { manager.Remove(callback); };
		});
	}

	protected ReactivePanelScope PanelRoot()
	{
		return new ReactivePanelScope(this);
	}

	protected sealed override void OnEnabled()
	{
		// component lifetimes are managed by their owning game objects, it doesn't make sense to bind it to the
		// lifetime of the currently executing effect
		_effectRoot = new Effect([StackTraceHidden] [DebuggerStepThrough]() =>
			{
				OnActivate();
				return null;
			},
			null,
			false);

		_effectRoot.SetDebugInfo(DisplayInfo.For(this).Name,
			DisplayInfo.For(this).Icon,
			new CallLocation(GetType(), nameof(OnActivate)),
			this);

		_effectRoot.Run();
	}

	protected sealed override void OnDisabled()
	{
		_renderEffectRoot?.Dispose();
		_renderEffectRoot = null;

		_effectRoot?.Dispose();
		_effectRoot = null;

		base.OnDisabled();
	}

	protected sealed override int BuildHash()
	{
		return _version;
	}

	/// <summary>
	/// Called inside an effect root when this component is enabled, allowing for effects to be created. When this
	/// component is disabled, the effect root (and all of its descendants) are disposed.
	/// </summary>
	protected virtual void OnActivate()
	{
	}
}
#endif