UI/SoftwareCursor.cs
using System;
using Sandbox.Rendering;

namespace BlockParty;

/// <summary>Replaces the OS cursor with a chunky pixel arrow. The arrow is painted through a
/// command list at Stage.AfterUI order 8000 — above every UI panel, but before the CRT pass at
/// 9000 — so it draws on top of everything yet is still curved together with the buttons it
/// points at: what the player sees under the tip is exactly what the engine hit-tests. In the
/// letterbox nothing is warped, so the arrow sits exactly on the real mouse position.
/// While the s&amp;box escape menu is open the arrow hides instead: that overlay composites after
/// every camera stage (so above the CRT pass AND above this arrow), and its own input context
/// outranks the game's — the OS cursor it summons is the real arrow/hand, which is why
/// Cursors.config only blanks the game-only "blank-*" names and leaves the standard ones
/// alone.</summary>
[Title( "Software Cursor" )]
[Category( "UI" )]
[Icon( "mouse" )]
public sealed class SoftwareCursor : Component
{
	// '#' = white fill, 'X' = black outline, ' ' = transparent. Classic arrow, hotspot at (0,0).
	private static readonly string[] ARROW =
	{
		"X",
		"XX",
		"X#X",
		"X##X",
		"X###X",
		"X####X",
		"X#####X",
		"X######X",
		"X###XXXX",
		"X#X#X",
		"XX X#X",
		"   X#X",
		"    XX",
	};

	// Pointing hand for interactive elements; the hotspot sits at the raised fingertip.
	private static readonly string[] HAND =
	{
		"   XX",
		"  X##X",
		"  X##X",
		"  X##X",
		"  X##X",
		"  X##XXXX",
		" XX##X#X#X",
		"X#X######X",
		"X########X",
		" X#######X",
		" X######X",
		"  X#####X",
		"  XXXXXXX",
	};
	private static readonly Vector2 HAND_HOTSPOT = new( 4f, 0f ); // fingertip centre, in cells

	private GameManager _manager;
	private CommandList _commands;
	private CameraComponent _attachedCamera;

	protected override void OnEnabled()
	{
		_manager = Components.Get<GameManager>( FindMode.InAncestors );
	}

	protected override void OnDisabled()
	{
		if ( _attachedCamera.IsValid() && _commands is not null )
			_attachedCamera.RemoveCommandList( _commands );
		_attachedCamera = null;
	}

	protected override void OnUpdate()
	{
		ApplyCursorState( out bool drawArrow );

		if ( _commands is null && drawArrow )
			return; // camera not ready yet; try again next frame

		_commands?.Reset();
		if ( !drawArrow ) return;

		// Interactive elements mark themselves with `cursor: blank-hand` in scss — a game-only
		// cursor name mapped to a blank texture in Cursors.config, so the style purely serves as
		// the "show the hand" marker here. (Standard names like "hand" stay unmapped so the s&box
		// escape menu keeps its real OS cursors.) Held state needs both
		// sources: presses on panels suppress the input action (the UI swallows the click) but
		// set :active on the pressed panel chain; background presses are the reverse.
		ScanPanels( out bool interactive, out bool panelPressed );
		bool held = panelPressed || Input.Down( "CursorClick" );

		string[] sprite = interactive ? HAND : ARROW;
		Vector2 hotspot = interactive ? HAND_HOTSPOT : Vector2.Zero;

		// Slightly smaller than the game grid reads better as a cursor. Cell edges are computed
		// from the shared formula and rounded to whole pixels, so neighbouring cells always abut
		// exactly — subpixel seams between cells otherwise get amplified into visible holes when
		// the quantize pass point-samples the frame.
		float px = MathF.Max( Screen.Height / Arena.HEIGHT, 1f ) * 0.75f;

		// Both cursors "press in" by a cell while held.
		float pressNudge = held ? px : 0f;
		float mouseX = Mouse.Position.x - hotspot.x * px + pressNudge;
		float mouseY = Mouse.Position.y - hotspot.y * px + pressNudge;
		using var paint = Painter.Begin( _commands );
		for ( int row = 0; row < sprite.Length; row++ )
		{
			float y0 = MathF.Round( mouseY + row * px );
			float y1 = MathF.Round( mouseY + (row + 1) * px );

			// Draw contiguous same-colour cells as one rect: fewer quads, no internal seams.
			string line = sprite[row];
			for ( int col = 0; col < line.Length; )
			{
				char cell = line[col];
				int end = col + 1;
				while ( end < line.Length && line[end] == cell ) end++;

				if ( cell != ' ' )
				{
					float x0 = MathF.Round( mouseX + col * px );
					float x1 = MathF.Round( mouseX + end * px );
					paint.Fill = cell == '#' ? Color.White : Color.Black;
					paint.Rect( new Rect( x0, y0, x1 - x0, y1 - y0 ) );
				}

				col = end;
			}
		}
	}

	// Re-assert the pointer type right before rendering too: clicking can make the engine
	// re-resolve the cursor after our OnUpdate ran, flashing the OS cursor for a frame.
	protected override void OnPreRender()
	{
		ApplyCursorState( out _ );
	}

	private void ApplyCursorState( out bool drawArrow )
	{
		var camera = _manager?.Camera;
		AttachCommandList( camera );

		// The drawn arrow is the only in-game cursor: inside the effect region it warps with the
		// UI it points at; in the letterbox nothing is warped so it sits exactly on the real
		// position. Never swapping to the OS cursor also avoids the engine flashing it on clicks.
		// The pointer IMAGE is hidden via CursorType — never MouseVisibility.Hidden, which is
		// mouse capture and relocates/locks the cursor. Panel `cursor:` styles override
		// CursorType, so game styles only ever use the blank-* names that map to a blank 1x1
		// texture in ProjectSettings/Cursors.config.
		Mouse.Visibility = MouseVisibility.Visible;
		Mouse.CursorType = "none";

		// While the s&box escape menu is up, hide the arrow: that overlay draws above the CRT
		// pass (and above this command list), so the arrow would sit warped UNDER the modal while
		// the menu hit-tests real positions. The menu's own input context outranks the game's and
		// summons the standard OS arrow/hand — the state forced above only touches the game
		// context, so the system cursor shows normally over the menu.
		drawArrow = camera.IsValid() && Mouse.Active && !(_manager?.IsPaused ?? false);
	}

	// One pass over all screen-panel trees (panel counts are small): `interactive` when the mouse
	// hovers any panel styled `cursor: blank-hand` — the game's marker for clickable things — and
	// `pressed` when any panel carries :active, i.e. the mouse is held down on it.
	private void ScanPanels( out bool interactive, out bool pressed )
	{
		interactive = false;
		pressed = false;

		foreach ( var screen in Scene.GetAllComponents<ScreenPanel>() )
		{
			var root = screen.GetPanel();
			if ( root is null ) continue;

			Walk( root, ref interactive, ref pressed );
			if ( interactive && pressed ) return;
		}
	}

	private static void Walk( Sandbox.UI.Panel panel, ref bool interactive, ref bool pressed )
	{
		var pseudo = panel.PseudoClass;
		if ( pseudo.HasFlag( Sandbox.UI.PseudoClass.Hover ) && panel.ComputedStyle?.Cursor == "blank-hand" )
			interactive = true;
		if ( pseudo.HasFlag( Sandbox.UI.PseudoClass.Active ) )
			pressed = true;
		if ( interactive && pressed ) return;

		foreach ( var child in panel.Children )
		{
			Walk( child, ref interactive, ref pressed );
			if ( interactive && pressed ) return;
		}
	}

	private void AttachCommandList( CameraComponent camera )
	{
		if ( !camera.IsValid() || _attachedCamera == camera ) return;

		if ( _attachedCamera.IsValid() && _commands is not null )
			_attachedCamera.RemoveCommandList( _commands );

		_commands ??= new CommandList( "BlockParty Software Cursor" );
		camera.AddCommandList( _commands, Sandbox.Rendering.Stage.AfterUI, 8000 );
		_attachedCamera = camera;
	}
}