Code/InteractiveComputer/Apps/PaintApp.cs
using System;
using Sandbox;
using Sandbox.UI;
using PaneOS.InteractiveComputer.Core;

namespace PaneOS.InteractiveComputer.Apps;

[ComputerApp( "system.paint", "Paint", Icon = "PT", SortOrder = 24 )]
public sealed class PaintApp : IComputerApp
{
	public ComputerAppSession Run( ComputerAppContext context )
	{
		return new ComputerAppSession
		{
			Title = "Paint",
			Icon = "PT",
			Content = new PaintPanel()
		};
	}
}

[StyleSheet( "InteractiveComputerApps.scss" )]
public sealed class PaintPanel : ComputerWarmupPanel
{
	private PaintCanvas canvas = null!;
	private int brushSize = 16;

	public PaintPanel()
	{
		AddClass( "paint-app" );
		BuildUi();
	}

	protected override void WarmupRefresh()
	{
		BuildUi();
	}

	private void BuildUi()
	{
		DeleteChildren( true );

		var toolbar = new Panel { Parent = this };
		toolbar.AddClass( "paint-toolbar" );

		foreach ( var color in new[] { "#ff595e", "#ffca3a", "#8ac926", "#1982c4", "#6a4c93" } )
		{
			var button = new Button() { Parent = toolbar };
			button.AddClass( "paint-color-button" );
			button.Style.BackgroundColor = Color.Parse( color );
			button.AddEventListener( "onclick", () => canvas.CurrentColor = color );
		}

		var clearButton = new Button( "Clear" ) { Parent = toolbar };
		clearButton.AddClass( "paint-clear" );
		var smallerBrushButton = new Button( "-" ) { Parent = toolbar };
		smallerBrushButton.AddClass( "paint-clear" );
		smallerBrushButton.AddEventListener( "onclick", () => SetBrushSize( brushSize - 2 ) );
		var largerBrushButton = new Button( "+" ) { Parent = toolbar };
		largerBrushButton.AddClass( "paint-clear" );
		largerBrushButton.AddEventListener( "onclick", () => SetBrushSize( brushSize + 2 ) );
		var sizeLabel = new Label( $"Brush {brushSize}px" ) { Parent = toolbar };
		sizeLabel.AddClass( "paint-size-label" );

		canvas = new PaintCanvas { Parent = this };
		canvas.AddClass( "paint-canvas" );
		canvas.BrushSize = brushSize;
		clearButton.AddEventListener( "onclick", canvas.ClearDots );
	}

	private void SetBrushSize( int nextSize )
	{
		brushSize = Math.Clamp( nextSize, 3, 36 );
		BuildUi();
	}
}

public sealed class PaintCanvas : Panel
{
	private bool isPainting;
	private Vector2? lastPaintPosition;

	public string CurrentColor { get; set; } = "#1982c4";
	public int BrushSize { get; set; } = 16;

	protected override void OnMouseDown( MousePanelEvent e )
	{
		base.OnMouseDown( e );
		var position = GetCanvasMousePosition( e );
		if ( !IsInsideCanvas( position ) )
			return;

		isPainting = true;
		lastPaintPosition = position;
		StampDot( position );
	}

	protected override void OnMouseMove( MousePanelEvent e )
	{
		base.OnMouseMove( e );

		if ( !isPainting )
			return;

		var position = GetCanvasMousePosition( e );
		if ( !IsInsideCanvas( position ) )
		{
			isPainting = false;
			lastPaintPosition = null;
			return;
		}

		foreach ( var point in ComputerPaintStrokePolicy.BuildStampPositions( lastPaintPosition, position, MathF.Max( 2f, BrushSize * 0.35f ) ) )
		{
			StampDot( point );
		}

		lastPaintPosition = position;
	}

	protected override void OnMouseUp( MousePanelEvent e )
	{
		base.OnMouseUp( e );
		isPainting = false;
		lastPaintPosition = null;
	}

	public void ClearDots()
	{
		DeleteChildren( true );
	}

	private void StampDot( Vector2 position )
	{
		if ( !IsInsideCanvas( position ) )
			return;

		var dot = new Panel { Parent = this };
		dot.AddClass( "paint-dot" );
		var radius = BrushSize * 0.5f;
		dot.Style.Left = Length.Pixels( position.x - radius );
		dot.Style.Top = Length.Pixels( position.y - radius );
		dot.Style.Width = Length.Pixels( BrushSize );
		dot.Style.Height = Length.Pixels( BrushSize );
		dot.Style.BackgroundColor = Color.Parse( CurrentColor );
	}

	private bool IsInsideCanvas( Vector2 position )
	{
		var rect = Box.Rect;
		return position.x >= 0f &&
			position.y >= 0f &&
			position.x <= rect.Width &&
			position.y <= rect.Height;
	}

	private Vector2 GetCanvasMousePosition( MousePanelEvent e )
	{
		return MousePosition;
	}
}