Code/UiPro/UiCanvas.cs

UI canvas component for the UiPro system. Manages child NodeComponent instances, computes canvas scale (constant or scale-with-screen-size), updates node layouts, collects input-targetable nodes, and issues GPU draw commands each frame via a CommandList and a vertex buffer.

Native InteropNetworking
using Sandbox.Rendering;
using System;
using System.Collections.Generic;

namespace Sandbox.UiPro;

public enum CanvasScaleMode
{
	ConstantPixelSize,
	ScaleWithScreenSize,
}

// I will add comments to this at some point :.)

[Title( "Canvas - UI Pro" ), Category( "UI Pro" ), Icon( "dashboard" )]
public class UiCanvas : Component
{
	private List<NodeComponent> _nodes;

	// For input system
	public IReadOnlyList<NodeComponent> TargetableNodes => _targetableNodes;
	private List<NodeComponent> _targetableNodes;

	[Property, Group( "Canvas Scaler" )]
	public CanvasScaleMode ScaleMode { get; set; } = CanvasScaleMode.ScaleWithScreenSize;

	[Property, Group( "Canvas Scaler" ), ShowIf( nameof( ScaleMode ), CanvasScaleMode.ScaleWithScreenSize )]
	public Vector2 ReferenceResolution { get; set; } = new Vector2( 1920, 1080 );

	[Property, Range( 0f, 1f ), Group( "Canvas Scaler" ), ShowIf( nameof( ScaleMode ), CanvasScaleMode.ScaleWithScreenSize )]
	public float MatchWidthOrHeight { get; set; } = 0.5f;

	public float ScaleFactor { get; private set; } = 1f;

	private CommandList _commands;
	private GpuBuffer<Vertex> _vertexBuffer;

	private static readonly Vertex[] UnitQuad =
	[
		new( new Vector3( 0f, 0f, 0f ) ),
		new( new Vector3( 1f, 0f, 0f ) ),
		new( new Vector3( 1f, 1f, 0f ) ),

		new( new Vector3( 0f, 0f, 0f ) ),
		new( new Vector3( 1f, 1f, 0f ) ),
		new( new Vector3( 0f, 1f, 0f ) ),
	];

	protected override void OnEnabled()
	{
		_nodes = new List<NodeComponent>();
		_targetableNodes = new List<NodeComponent>();

		_commands = new CommandList();
		Scene.Camera?.AddCommandList( _commands, Stage.AfterPostProcess );

		_vertexBuffer = new GpuBuffer<Vertex>( 6, GpuBuffer.UsageFlags.Vertex );
		_vertexBuffer.SetData( UnitQuad );
	}

	protected override void OnDisabled()
	{
		_nodes = null;
		_targetableNodes = null;

		if ( _commands != null )
		{
			Scene.Camera?.RemoveCommandList( _commands );
			_commands = null;
		}

		_vertexBuffer?.Dispose();
		_vertexBuffer = null;
	}

	protected override void OnUpdate()
	{
		UpdateNodes();
	}

	protected override void OnPreRender()
	{
		_commands.Reset();
		DrawNodes();
	}

	private void UpdateNodes()
	{
		_nodes.Clear();
		_targetableNodes.Clear();

		ScaleFactor = ComputeScaleFactor();

		Vector2 virtualSize = Screen.Size / ScaleFactor;

		foreach ( GameObject child in GameObject.Children )
			UpdateNode( child, NodeLayout.GetRootLayout( virtualSize ) );
	}

	private float ComputeScaleFactor()
	{
		if ( ScaleMode == CanvasScaleMode.ConstantPixelSize )
			return 1f;

		Vector2 screen = Screen.Size;
		Vector2 reference = ReferenceResolution;

		if ( reference.x <= 0f || reference.y <= 0f || screen.x <= 0f || screen.y <= 0f )
			return 1f;

		float logWidth = MathF.Log2( screen.x / reference.x );
		float logHeight = MathF.Log2( screen.y / reference.y );
		float logScale = MathX.Lerp( logWidth, logHeight, MatchWidthOrHeight );

		return MathF.Pow( 2f, logScale );
	}

	private void UpdateNode( GameObject nodeGameObject, NodeLayout parentLayout )
	{
		NodeComponent node = nodeGameObject.GetComponent<NodeComponent>();

		if ( node != null )
		{
			node.Update( parentLayout, ScaleFactor );

			_nodes.Add( node );

			if ( node.ReceivePointerInput )
				_targetableNodes.Add( node );

			foreach ( GameObject child in nodeGameObject.Children )
				UpdateNode( child, node.Layout );
		}
		else
		{
			foreach ( GameObject child in nodeGameObject.Children )
				UpdateNode( child, parentLayout );
		}
	}

	public Vector2 ScreenToCanvas( Vector2 screenPosition )
	{
		float scale = ScaleFactor <= 0f ? 1f : ScaleFactor;
		return screenPosition / scale;
	}

	private void DrawNodes()
	{
		Vector2 virtualViewport = Screen.Size / ScaleFactor;

		foreach ( NodeComponent node in _nodes )
			_commands.Draw( _vertexBuffer, NodeComponent.Material, 0, 6, node.GetRenderAttributes( virtualViewport ), Graphics.PrimitiveType.Triangles );
	}
}