PostprocessablePanel.razor.cs
using Sandbox;
using Sandbox.UI;
using System;

namespace PostprocessPanel;

/// <summary>
/// A panel that allows to modify the rendered Body with
/// compute shaders and stuffs.
/// 
/// A few quick mentions:
/// 
/// - The body panel should not be transformed if the
///		padding is not big enough. This will cause the
///		panel to be cut off.
///		
///	- When setting the texture padding, make sure to take
///		the aspect ratio of the body panel into account!
///		
/// - The padding is automatically scaled with the screen!
/// 
///	- The body panel does not support backdrop filters
///		as they require the rendered scene to be in
///		the background. However, the texture that this is
///		rendered onto does not contain it!
/// </summary>
public sealed partial class PostprocessablePanel : Panel, IRenderingRootAccessor
{
	/// <summary>
	/// Body fragment. This panel will be the one that gets
	/// rendered in a separate root.
	/// </summary>
	public RenderFragment Body { get; set; }

	/// <summary>
	/// Display fragment. This panel will be used to display
	/// the rendered body onto after it was post processed.
	/// </summary>
	public RenderFragment Display { get; set; }

	/// <summary>
	/// Reference to the attributes that holds the raw texture
	/// of the body panel as well as other stuffs that might
	/// be necessary for the compute shaders.
	/// 
	/// The key for the raw body texture is "RawTexture" and
	/// the finalised texture is expected to have "ProcessedTexture"
	/// by default. This can be changed with SetProcessedTextureName()!
	/// 
	/// This is cleared between each frame!
	/// </summary>
	public RenderAttributes Attributes => _root.Attributes;

	/// <summary>
	/// Do we have our display panel?
	/// </summary>
	public bool HasDisplayPanel => DisplayPanel is not null;

	/// <summary>
	/// Do we have our body panel, that is rendered to
	/// a texture?
	/// </summary>
	public bool HasBodyPanel => _root.IsValid() && _root.HasBodyPanel;

	/// <summary>
	/// Think of this as the IsValid field. Tells if the panel
	/// has everything it needs to do its job.
	/// </summary>
	public bool IsReady => this.IsValid() && _root.IsValid();

	/// <summary>
	/// How big is our texture that we rendered onto. This
	/// is decided by how big the panel itself is. This includes
	/// the padding!
	/// </summary>
	public Vector2Int TextureSize => _root.TextureSize;

	/// <summary>
	/// Add extra pixels to the render texture in case
	/// the effect requires so. Is clamped if either axis
	/// are below 0!
	/// </summary>
	public Vector2Int TexturePadding
	{
		get => _root.TexturePadding;
		set => _root.TexturePadding = value;
	}

	/// <summary>
	/// Dispatched after we're done rendering the body panel
	/// and saved it into the attributes. This is the time
	/// to dispatch the compute shaders to modify the texture.
	/// </summary>
	public Action OnRendering
	{
		get => _root.OnRendering;
		set
		{
			_root.HasRenderingCallback = true;
			_root.OnRendering = value;
		}
	}

	private string _processedLookupName;

	internal Panel DisplayPanel => GetChild( 0 );

	private RenderingRoot _root;

	protected override void OnAfterTreeRender( bool firstTime )
	{
		if ( firstTime is false )
			return;

		CreateRoot();
		SetProcessedTextureName();
	}

	public override void Tick()
	{
		if ( _root.IsValid() is false || _root.HasBodyPanel is false || HasDisplayPanel is false )
			return;

		_root.CopyPseudoClasses( this.PseudoClass );

		Texture finalTexture = Attributes.GetTexture( _processedLookupName );

		DisplayPanel.Style.SetBackgroundImage( finalTexture );
	}

	public override void OnDeleted()
	{
		_root?.Delete( immediate: true );
	}

	public override int GetHashCode() => HashCode.Combine( _root.IsValid() );

	/// <summary>
	/// Update the scale and opacity from a screen panel
	/// </summary>
	/// <param name="screenPanel"></param>
	public void UpdateRootSettingsFrom( ScreenPanel screenPanel )
	{
		UpdateRootSettings(
			screenPanel.AutoScreenScale,
			screenPanel.Scale,
			screenPanel.ScaleStrategy,
			screenPanel.Opacity
		);
	}

	/// <summary>
	/// Allows updating the root settings such as the manual scale,
	/// opacity and the scaling strategy. This is useful if the screen panel
	/// or world panel uses different settings
	/// </summary>
	/// <param name="autoScale">Determine scale with the scaling strategy or with the manual scale</param>
	/// <param name="manualScale">If auto scale is off use this value to scale the panel</param>
	/// <param name="scaleStrategy">If auto scale is on, depending on the value set figure out scaling</param>
	/// <param name="manualOpacity">What opacity should the body panel be rendered?</param>
	public void UpdateRootSettings(
		bool autoScale = true,
		float manualScale = 1f,
		ScreenPanel.AutoScale scaleStrategy = ScreenPanel.AutoScale.ConsistentHeight,
		float manualOpacity = 1f
	)
	{
		_root.AutoScreenScale = autoScale;
		_root.ManualScale = manualScale;
		_root.ScaleStrategy = scaleStrategy;
		_root.ManualOpacity = manualOpacity;
	}

	/// <summary>
	/// Dispatches the compute shader with the attributes
	/// and with the correct amount of threads.
	/// </summary>
	/// <param name="compute"></param>
	public void DispatchCompute( ComputeShader compute )
	{
		compute.DispatchWithAttributes( Attributes, _root.TextureSize.x, _root.TextureSize.y, 1 );
	}

	/// <summary>
	/// Sets the name of the processed texture what will be
	/// used to look it up inside of the render attributes.
	/// 
	/// By default this is called ProcessedTexture. This
	/// method exists in case multiple passes are required.
	/// </summary>
	/// <param name="name"></param>
	public void SetProcessedTextureName( string name = "ProcessedTexture" )
	{
		_processedLookupName = name;

		if ( _root.IsValid() )
		{
			_root.ProcessedTextureName = _processedLookupName;
		}
	}

	private void CreateRoot()
	{
		_root = new( Body, Scene );
	}
}