CrtView.cs
namespace PixelPusher;

[Library( "crtview" )]
public sealed class CrtView : Panel
{
	public const int VW = 640;
	public const int VH = 360;

	Texture _tex;
	Color32[] _buf;
	int _tw;
	int _th;
	bool _held;
	bool _heldRight;
	bool _panning;
	int _hx;
	int _hy;
	int _panX;
	int _panY;

	public static bool PointerOn { get; private set; }
	public static int HoverX { get; private set; }
	public static int HoverY { get; private set; }

	public CrtView()
	{
		AddClass( "crtview" );
		Style.Width = Length.Fraction( 1f );
		Style.Height = Length.Fraction( 1f );
	}

	public override void Tick()
	{
		base.Tick();
		Ensure( VW, VH );
		if ( _buf is null )
			return;

		Clear( new Color32( 4, 19, 28, 255 ) );

		var screen = GameFlow.Screen;
		if ( screen == GameScreen.Pixel )
			DrawPixel();
		else if ( IsIso( screen ) )
			DrawIso( IsPlaying3D( screen ) );
		else if ( screen is GameScreen.Stage or GameScreen.Play2D or GameScreen.Cleared or GameScreen.Pause )
			DrawWorld( IsPlaying2D( screen ) );
		else if ( screen == GameScreen.Chip )
			DrawChip();
		else if ( screen == GameScreen.Library )
			DrawWorld( false );

		PointerOn = HasHovered || _held || _panning;
		if ( MapMouse( MousePosition, out var hx, out var hy ) )
		{
			HoverX = hx;
			HoverY = hy;
		}
		if ( _held )
			PumpHeld();
		DrawCursorBox();

		try
		{
			_tex.Update( _buf, 0, 0, _tw, _th );
			Style.BackgroundImage = _tex;
		}
		catch
		{
			// ignore a missed frame
		}
	}

	protected override void OnMouseDown( MousePanelEvent e )
	{
		base.OnMouseDown( e );
		if ( !MapMouse( e.LocalPosition, out var mx, out var my ) )
			return;
		var right = e.MouseButton == MouseButtons.Right;
		var left = e.MouseButton == MouseButtons.Left;
		var middle = e.MouseButton == MouseButtons.Middle;
		if ( middle )
		{
			_panning = true;
			_panX = mx;
			_panY = my;
			e.StopPropagation();
			return;
		}
		if ( !left && !right )
			return;
		_held = true;
		_heldRight = right;
		_hx = mx;
		_hy = my;
		e.StopPropagation();
		Dispatch( mx, my, left, right, true, false );
		if ( GameFlow.Screen == GameScreen.Pixel )
			Coach.OnPaint();
		if ( GameFlow.Screen == GameScreen.Stage )
			Coach.OnStagePaint();
	}

	protected override void OnMouseMove( MousePanelEvent e )
	{
		base.OnMouseMove( e );
		if ( !MapMouse( e.LocalPosition, out var mx, out var my ) )
			return;
		HoverX = mx;
		HoverY = my;
		if ( _panning )
		{
			StudioMouse.Pan( mx - _panX, my - _panY );
			_panX = mx;
			_panY = my;
			return;
		}
		if ( !_held )
			return;
		_hx = mx;
		_hy = my;
		Dispatch( mx, my, !_heldRight, _heldRight, false, false );
	}

	protected override void OnMouseUp( MousePanelEvent e )
	{
		base.OnMouseUp( e );
		if ( _panning )
		{
			_panning = false;
			e.StopPropagation();
			return;
		}
		if ( !_held )
			return;
		MapMouse( e.LocalPosition, out var mx, out var my );
		Dispatch( mx, my, false, false, false, true );
		_held = false;
		e.StopPropagation();
	}

	public override void OnMouseWheel( Vector2 delta )
	{
		var dir = delta.y != 0 ? (delta.y > 0 ? 1 : -1) : (delta.x > 0 ? 1 : -1);
		StudioMouse.Wheel( dir, HoverX, HoverY );
	}

	protected override void OnBack( PanelEvent e )
	{
		StudioMouse.Back();
		e.StopPropagation();
	}

	protected override void OnForward( PanelEvent e )
	{
		StudioMouse.Forward();
		e.StopPropagation();
	}

	void PumpHeld()
	{
		if ( !MapMouse( MousePosition, out var mx, out var my ) )
			return;
		_hx = mx;
		_hy = my;
		Dispatch( mx, my, !_heldRight, _heldRight, false, false );
	}

	bool MapMouse( Vector2 local, out int mx, out int my )
	{
		mx = 0;
		my = 0;
		var rect = Box.Rect;
		if ( rect.Width < 2 || rect.Height < 2 )
			return false;
		var scale = MathF.Min( rect.Width / VW, rect.Height / VH );
		if ( scale < 0.01f )
			return false;
		var ox = (rect.Width - VW * scale) * 0.5f;
		var oy = (rect.Height - VH * scale) * 0.5f;
		mx = (int)MathF.Floor( (local.x - ox) / scale );
		my = (int)MathF.Floor( (local.y - oy) / scale );
		return (uint)mx < VW && (uint)my < VH;
	}

	void Dispatch( int mx, int my, bool left, bool right, bool down, bool released )
	{
		if ( GameFlow.Screen == GameScreen.Pixel )
		{
			MapPixel( mx, my, out var px, out var py );
			PixelStudio.Pointer( px, py, left, right, down, released );
			return;
		}

		if ( GameFlow.Screen == GameScreen.Stage )
		{
			MapStage( mx, my, out var tx, out var ty );
			StageStudio.Pointer( tx, ty, left, right, down, released );
			return;
		}

		if ( GameFlow.Screen == GameScreen.Chip )
		{
			if ( down )
				ChipStudio.Hit( mx, my, right );
			else if ( left || right )
				ChipStudio.DragPaint( mx, my, right );
		}
	}

	void MapPixel( int mx, int my, out int px, out int py )
	{
		var doc = Desk.Sprite;
		var zoom = Math.Max( 1, PixelStudio.Zoom );
		var cw = doc?.Width ?? 16;
		var ch = doc?.Height ?? 16;
		var ox = (VW - cw * zoom) / 2 - PixelStudio.PanX;
		var oy = (VH - ch * zoom) / 2 - PixelStudio.PanY;
		px = (mx - ox) / zoom;
		py = (my - oy) / zoom;
	}

	void MapStage( int mx, int my, out int tx, out int ty )
	{
		var stage = Desk.Stage;
		var ts = stage?.TileSize ?? 16;
		var zoom = Math.Max( 1, StageStudio.Zoom );
		var wx = StageStudio.CamX + mx / (float)zoom;
		var wy = StageStudio.CamY + my / (float)zoom;
		tx = (int)MathF.Floor( wx / ts );
		ty = (int)MathF.Floor( wy / ts );
	}

	void DrawPixel()
	{
		var doc = Desk.Sprite;
		if ( doc is null )
			return;

		var zoom = Math.Max( 1, PixelStudio.Zoom );
		var ox = (VW - doc.Width * zoom) / 2 - PixelStudio.PanX;
		var oy = (VH - doc.Height * zoom) / 2 - PixelStudio.PanY;

		Checker( ox, oy, doc.Width * zoom, doc.Height * zoom );

		if ( PixelRef.Visible && PixelRef.HasImage )
		{
			var fade = PixelRef.Opacity / 100f;
			for ( var y = 0; y < doc.Height; y++ )
			for ( var x = 0; x < doc.Width; x++ )
			{
				var ghost = PixelRef.Sample( x, y, doc.Width, doc.Height );
				Color ga = ghost;
				if ( ga.a < 0.04f )
					continue;
				BlendRect( ox + x * zoom, oy + y * zoom, zoom, zoom, ghost, fade );
			}
		}

		if ( PixelStudio.Onion && doc.FrameCount > 1 )
		{
			for ( var y = 0; y < doc.Height; y++ )
			for ( var x = 0; x < doc.Width; x++ )
			{
				var ghost = doc.OnionAt( x, y );
				Color ga = ghost;
				if ( ga.a < 0.04f )
					continue;
				FillRect( ox + x * zoom, oy + y * zoom, zoom, zoom, ghost );
			}
		}

		for ( var y = 0; y < doc.Height; y++ )
		for ( var x = 0; x < doc.Width; x++ )
		{
			Color32 c = doc.ColorAt( x, y );
			Color cc = c;
			if ( cc.a < 0.04f )
				continue;
			FillRect( ox + x * zoom, oy + y * zoom, zoom, zoom, c );
		}

		if ( PixelStudio.Grid && zoom >= 4 )
		{
			var grid = new Color32( 255, 255, 255, 28 );
			for ( var x = 0; x <= doc.Width; x++ )
				VLine( ox + x * zoom, oy, doc.Height * zoom, grid );
			for ( var y = 0; y <= doc.Height; y++ )
				HLine( ox, oy + y * zoom, doc.Width * zoom, grid );
		}

		if ( PixelStudio.Preview( out var x0, out var y0, out var x1, out var y1 ) )
		{
			Color src = Palettes.Default.Colors[6];
			var preview = new Color32( (byte)(src.r * 255f), (byte)(src.g * 255f), (byte)(src.b * 255f), 180 );
			var rx0 = Math.Min( x0, x1 );
			var ry0 = Math.Min( y0, y1 );
			var rx1 = Math.Max( x0, x1 );
			var ry1 = Math.Max( y0, y1 );
			Rect( ox + rx0 * zoom, oy + ry0 * zoom, (rx1 - rx0 + 1) * zoom, (ry1 - ry0 + 1) * zoom, preview );
		}

		if ( PixelStudio.PadAim || Input.UsingController )
		{
			var cx = ox + PixelStudio.CursorX * zoom;
			var cy = oy + PixelStudio.CursorY * zoom;
			Rect( cx, cy, zoom, zoom, new Color32( 245, 248, 254, 255 ) );
		}
		else
		{
			MapPixel( HoverX, HoverY, out var hx, out var hy );
			PixelStudio.HoverPx = hx;
			PixelStudio.HoverPy = hy;
			if ( (uint)hx < (uint)doc.Width && (uint)hy < (uint)doc.Height && zoom >= 2 )
			{
				Color fc = doc.Palette[Math.Clamp( PixelStudio.Fg, 0, doc.Palette.Length - 1 )];
				var ghost = new Color32(
					(byte)MathF.Round( fc.r * 255f ),
					(byte)MathF.Round( fc.g * 255f ),
					(byte)MathF.Round( fc.b * 255f ),
					90 );
				var r = Math.Max( 1, PixelStudio.Brush );
				for ( var dy = -r + 1; dy < r; dy++ )
				for ( var dx = -r + 1; dx < r; dx++ )
				{
					if ( PixelStudio.Circle && dx * dx + dy * dy >= r * r )
						continue;
					var gx = hx + dx;
					var gy = hy + dy;
					if ( (uint)gx >= (uint)doc.Width || (uint)gy >= (uint)doc.Height )
						continue;
					FillRect( ox + gx * zoom, oy + gy * zoom, zoom, zoom, ghost );
					if ( PixelStudio.Mirror )
					{
						var mx = doc.Width - 1 - gx;
						FillRect( ox + mx * zoom, oy + gy * zoom, zoom, zoom, ghost );
					}
				}
			}
		}
	}

	void DrawWorld( bool playing )
	{
		var stage = Desk.Stage;
		var tiles = Desk.Tileset;
		if ( stage is null )
			return;

		var ts = stage.TileSize;
		var zoom = playing ? 2 : Math.Max( 1, StageStudio.Zoom );
		var camX = playing ? PlaySim.CamX : StageStudio.CamX;
		var camY = playing ? PlaySim.CamY : StageStudio.CamY;

		var worldW = stage.Width * ts;
		var worldH = stage.Height * ts;

		var x0 = Math.Max( 0, (int)MathF.Floor( camX / ts ) - 1 );
		var y0 = Math.Max( 0, (int)MathF.Floor( camY / ts ) - 1 );
		var x1 = Math.Min( stage.Width - 1, (int)MathF.Floor( (camX + VW / (float)zoom) / ts ) + 1 );
		var y1 = Math.Min( stage.Height - 1, (int)MathF.Floor( (camY + VH / (float)zoom) / ts ) + 1 );

		for ( var ty = y0; ty <= y1; ty++ )
		for ( var tx = x0; tx <= x1; tx++ )
		{
			var id = stage.TileAt( tx, ty );
			if ( id == 0 )
				continue;
			var sx = (int)MathF.Round( (tx * ts - camX) * zoom );
			var sy = (int)MathF.Round( (ty * ts - camY) * zoom );
			BlitTile( tiles, id, sx, sy, zoom );
		}

		foreach ( var e in stage.Entities )
		{
			var sx = (int)MathF.Round( (e.Tx * ts - camX) * zoom );
			var sy = (int)MathF.Round( (e.Ty * ts - camY) * zoom );
			if ( e.Kind == "coin" )
			{
				if ( playing )
					continue;
				StarterPack.BlitCoin( _buf, _tw, _th, sx, sy, Time.Now );
			}
			else if ( e.Kind == "start" && !playing )
				Rect( sx + 2, sy + 2, ts * zoom - 4, ts * zoom - 4, new Color32( 77, 255, 200, 200 ) );
			else if ( e.Kind == "exit" )
				FillRect( sx + 3, sy, ts * zoom - 6, ts * zoom, new Color32( 255, 77, 141, 220 ) );
			else if ( e.Kind == "spring" && !playing )
				FillRect( sx + 2, sy + ts * zoom / 2, ts * zoom - 4, ts * zoom / 2, new Color32( 255, 209, 102, 220 ) );
			else if ( e.Kind == "check" && !playing )
				FillRect( sx + 5, sy + 2, 6 * zoom, ts * zoom - 4, new Color32( 77, 255, 200, 220 ) );
			else if ( e.Kind == "sign" && !playing )
			{
				FillRect( sx + 4, sy + 2, 8 * zoom, 10 * zoom, new Color32( 255, 209, 102, 230 ) );
				FillRect( sx + 7, sy + 12 * zoom, 2 * zoom, 4 * zoom, new Color32( 192, 96, 64, 255 ) );
			}
		}

		if ( playing )
		{
			foreach ( var e in stage.Entities )
			{
				if ( e.Kind != "coin" || PlaySim.Collected( e.Tx, e.Ty ) )
					continue;
				var sx = (int)MathF.Round( (e.Tx * ts - camX) * zoom );
				var sy = (int)MathF.Round( (e.Ty * ts - camY) * zoom );
				StarterPack.BlitCoin( _buf, _tw, _th, sx, sy, PlaySim.TimePlayed );
			}

			var px = (int)MathF.Round( (PlaySim.Px - camX) * zoom ) - 2;
			var py = (int)MathF.Round( (PlaySim.Py - camY) * zoom ) - 1;
			var frame = PlaySim.Grounded && MathF.Abs( PlaySim.Vx ) > 20f && ((int)(PlaySim.Anim * 8f) & 1) == 1 ? 1 : 0;
			if ( PlaySim.Dead )
				FillRect( px, py, 16, 16, new Color32( 255, 77, 141, 180 ) );
			else
				StarterPack.BlitTerry( _buf, _tw, _th, px, py, PlaySim.FaceLeft, frame );
		}
		else if ( StageStudio.Grid && zoom >= 2 )
		{
			var grid = new Color32( 255, 255, 255, 22 );
			var gx0 = (int)MathF.Round( (x0 * ts - camX) * zoom );
			var gy0 = (int)MathF.Round( (y0 * ts - camY) * zoom );
			for ( var tx = x0; tx <= x1 + 1; tx++ )
				VLine( (int)MathF.Round( (tx * ts - camX) * zoom ), gy0, (y1 - y0 + 2) * ts * zoom, grid );
			for ( var ty = y0; ty <= y1 + 1; ty++ )
				HLine( gx0, (int)MathF.Round( (ty * ts - camY) * zoom ), (x1 - x0 + 2) * ts * zoom, grid );
		}

		if ( !playing && StageStudio.Preview( out var ax, out var ay, out var bx, out var by ) )
		{
			var px0 = Math.Min( ax, bx );
			var py0 = Math.Min( ay, by );
			var px1 = Math.Max( ax, bx );
			var py1 = Math.Max( ay, by );
			var sx = (int)MathF.Round( (px0 * ts - camX) * zoom );
			var sy = (int)MathF.Round( (py0 * ts - camY) * zoom );
			Rect( sx, sy, (px1 - px0 + 1) * ts * zoom, (py1 - py0 + 1) * ts * zoom, new Color32( 50, 115, 235, 200 ) );
		}

		if ( !playing && (StageStudio.PadAim || Input.UsingController) )
		{
			var sx = (int)MathF.Round( (StageStudio.CursorTx * ts - camX) * zoom );
			var sy = (int)MathF.Round( (StageStudio.CursorTy * ts - camY) * zoom );
			Rect( sx, sy, ts * zoom, ts * zoom, new Color32( 50, 115, 235, 255 ) );
		}

		_ = worldW;
		_ = worldH;
	}

	static bool IsPlaying2D( GameScreen screen )
		=> screen == GameScreen.Play2D
		|| (screen == GameScreen.Pause && GameFlow.ResumeTo == GameScreen.Play2D)
		|| (screen == GameScreen.Cleared && !PlaySim.Walk3D);

	static bool IsPlaying3D( GameScreen screen )
		=> screen == GameScreen.Play3D
		|| (screen == GameScreen.Pause && GameFlow.ResumeTo == GameScreen.Play3D)
		|| (screen == GameScreen.Cleared && PlaySim.Walk3D);

	static bool IsIso( GameScreen screen )
		=> screen == GameScreen.World
		|| screen == GameScreen.Play3D
		|| (screen == GameScreen.Pause && GameFlow.ResumeTo is GameScreen.World or GameScreen.Play3D)
		|| (screen == GameScreen.Cleared && PlaySim.Walk3D);

	void DrawIso( bool playing )
	{
		var stage = Desk.Stage;
		var tiles = Desk.Tileset;
		if ( stage is null )
			return;

		var ts = stage.TileSize;
		var scale = playing ? 2f : Math.Max( 1, WorldStudio.Zoom );
		var camX = playing ? PlaySim.CamX : WorldStudio.CamX;
		var camY = playing ? PlaySim.CamY : WorldStudio.CamY;
		var slab = WorldStudio.SlabDepth;

		for ( var ty = 0; ty < stage.Height; ty++ )
		for ( var tx = 0; tx < stage.Width; tx++ )
		{
			var id = stage.TileAt( tx, ty );
			if ( id == 0 )
				continue;
			var wx = tx * ts;
			var wy = ty * ts;
			var solid = tiles is not null && (tiles.IsSolid( id ) || tiles.IsOneWay( id ) || tiles.IsHazard( id ) || tiles.IsSpring( id ));
			DrawSlab( tiles, id, wx, wy, solid ? slab : slab * 0.35f, scale, camX, camY, solid );
		}

		foreach ( var e in stage.Entities )
		{
			if ( playing && e.Kind == "coin" && PlaySim.Collected( e.Tx, e.Ty ) )
				continue;
			var wx = e.Tx * ts;
			var wy = e.Ty * ts;
			WorldStudio.Project( wx, wy, slab * 0.45f, out var sx, out var sy, scale, camX, camY );
			if ( e.Kind == "coin" )
				StarterPack.BlitCoin( _buf, _tw, _th, sx, sy, playing ? PlaySim.TimePlayed : Time.Now );
			else if ( e.Kind == "exit" )
				FillRect( sx + 3, sy, ts - 6, ts, new Color32( 255, 77, 141, 220 ) );
			else if ( e.Kind == "spring" )
				FillRect( sx + 2, sy + ts / 2, ts - 4, ts / 2, new Color32( 255, 209, 102, 220 ) );
			else if ( e.Kind == "start" && !playing )
				Rect( sx + 2, sy + 2, ts - 4, ts - 4, new Color32( 77, 255, 200, 200 ) );
			else if ( e.Kind == "check" )
				FillRect( sx + 5, sy + 2, 6, ts - 4, new Color32( 77, 255, 200, 220 ) );
			else if ( e.Kind == "sign" )
			{
				FillRect( sx + 4, sy + 2, 8, 10, new Color32( 255, 209, 102, 230 ) );
				FillRect( sx + 7, sy + 12, 2, 4, new Color32( 192, 96, 64, 255 ) );
			}
		}

		if ( playing )
		{
			WorldStudio.Project( PlaySim.Px - 2, PlaySim.Py - 1, PlaySim.Pz, out var px, out var py, scale, camX, camY );
			var frame = PlaySim.Grounded && MathF.Abs( PlaySim.Vx ) > 20f && ((int)(PlaySim.Anim * 8f) & 1) == 1 ? 1 : 0;
			if ( PlaySim.Dead )
				FillRect( px, py, 16, 16, new Color32( 255, 77, 141, 180 ) );
			else
				StarterPack.BlitTerry( _buf, _tw, _th, px, py, PlaySim.FaceLeft, frame );
		}

		PixFont.Draw( _buf, _tw, _th, 4, 4, playing ? "WALK" : "WORLD STUDIO", new Color32( 77, 255, 200, 220 ), 1 );
	}

	void DrawSlab( TilesetDoc tiles, int id, float wx, float wy, float depth, float scale, float camX, float camY, bool solid )
	{
		WorldStudio.Project( wx, wy, 0, out var fx, out var fy, scale, camX, camY );
		WorldStudio.Project( wx, wy, depth, out var bx, out var by, scale, camX, camY );
		var ts = (int)MathF.Round( (Desk.Stage?.TileSize ?? 16) * scale );
		if ( ts < 4 )
			ts = 4;

		var avg = AverageTile( tiles, id );
		var top = Mul( avg, 1.25f );
		var side = Mul( avg, 0.55f );

		if ( solid )
		{
			FillParallelogram( fx + ts, fy, bx + ts, by, ts, side );
			FillParallelogram( fx, fy, bx, by, ts, top );
		}

		if ( tiles?.Sheet is not null )
			BlitTile( tiles, id, fx, fy, Math.Max( 1, (int)scale ) );
		else
			FillRect( fx, fy, ts, ts, avg );
	}

	void DrawChip()
	{
		var doc = Desk.Chip;
		FillRect( 0, 0, VW, VH, new Color32( 17, 20, 29, 255 ) );
		PixFont.Draw( _buf, _tw, _th, 16, 14, "CHIP STUDIO", new Color32( 50, 115, 235, 255 ), 3 );
		PixFont.Draw( _buf, _tw, _th, 16, 42, doc is null ? "NO CHIP" : $"{doc.Bpm} BPM   SPACE PLAY   CLICK NOTE   RMB PITCH", new Color32( 198, 212, 238, 255 ), 1 );

		if ( doc is null )
			return;

		var gx = ChipStudio.GridX;
		var gy = ChipStudio.GridY;
		var cw = ChipStudio.CellW;
		var chh = ChipStudio.CellH;
		var head = ChipStudio.Playing ? ChipStudio.Head : -1;
		var blue = new Color32( 50, 115, 235, 255 );
		var dim = new Color32( 22, 28, 40, 255 );
		var beat = new Color32( 28, 36, 52, 255 );
		var ink = new Color32( 245, 248, 254, 255 );
		var mute = new Color32( 255, 93, 106, 255 );

		for ( var step = 0; step < ChipDoc.Steps; step++ )
		{
			var x = gx + step * cw;
			if ( step % 4 == 0 )
				PixFont.Draw( _buf, _tw, _th, x + 6, gy - 14, (step + 1).ToString(), new Color32( 143, 154, 174, 255 ), 1 );
			if ( step == head )
				FillRect( x - 1, gy - 4, cw, ChipDoc.Channels * chh + 4, new Color32( 50, 115, 235, 40 ) );
		}

		for ( var ch = 0; ch < ChipDoc.Channels; ch++ )
		{
			var rowY = gy + ch * chh;
			var name = ChipStudio.ChannelNames[ch];
			var muted = ChipStudio.Mute[ch];
			PixFont.Draw( _buf, _tw, _th, 10, rowY + 22, name, muted ? mute : ink, 1 );
			if ( muted )
				PixFont.Draw( _buf, _tw, _th, 10, rowY + 36, "MUTE", mute, 1 );

			for ( var step = 0; step < ChipDoc.Steps; step++ )
			{
				var x = gx + step * cw;
				var note = doc.Get( ch, step );
				var on = note > 0;
				var cursor = ChipStudio.CursorCh == ch && ChipStudio.CursorStep == step;
				var bg = step % 4 == 0 ? beat : dim;
				FillRect( x + 1, rowY + 2, cw - 3, chh - 6, bg );
				if ( on )
				{
					var h = Math.Max( 8, note * (chh - 16) / 12 );
					var y = rowY + (chh - 8) - h;
					FillRect( x + 4, y, cw - 9, h, muted ? mute : blue );
					var label = note < ChipStudio.NoteNames.Length ? ChipStudio.NoteNames[note] : note.ToString();
					PixFont.Draw( _buf, _tw, _th, x + 6, rowY + 6, label, ink, 1 );
				}
				if ( cursor )
					Rect( x + 1, rowY + 2, cw - 3, chh - 6, ink );
			}
		}
	}

	void FillParallelogram( int x0, int y0, int x1, int y1, int h, Color32 c )
	{
		var steps = Math.Max( 1, Math.Abs( x1 - x0 ) );
		for ( var i = 0; i <= steps; i++ )
		{
			var t = i / (float)steps;
			var x = (int)MathF.Round( x0 + (x1 - x0) * t );
			var y = (int)MathF.Round( y0 + (y1 - y0) * t );
			VLine( x, y, h, c );
		}
	}

	Color32 AverageTile( TilesetDoc tiles, int id )
	{
		if ( tiles?.Sheet is null || id <= 0 )
			return new Color32( 61, 77, 138, 255 );
		tiles.TileOrigin( id, out var ox, out var oy );
		var r = 0f;
		var g = 0f;
		var b = 0f;
		var n = 0;
		var ts = tiles.TileSize;
		for ( var y = 0; y < ts; y += 2 )
		for ( var x = 0; x < ts; x += 2 )
		{
			Color c = tiles.Sheet.ColorAt( ox + x, oy + y );
			if ( c.a < 0.04f )
				continue;
			r += c.r;
			g += c.g;
			b += c.b;
			n++;
		}
		if ( n == 0 )
			return new Color32( 61, 77, 138, 255 );
		return new Color32(
			(byte)MathF.Round( r / n * 255f ),
			(byte)MathF.Round( g / n * 255f ),
			(byte)MathF.Round( b / n * 255f ),
			255 );
	}

	static Color32 Mul( Color32 c, float m )
	{
		Color col = c;
		return new Color32(
			(byte)MathF.Round( Math.Clamp( col.r * m, 0f, 1f ) * 255f ),
			(byte)MathF.Round( Math.Clamp( col.g * m, 0f, 1f ) * 255f ),
			(byte)MathF.Round( Math.Clamp( col.b * m, 0f, 1f ) * 255f ),
			255 );
	}

	void DrawCursorBox()
	{
		if ( !HasHovered )
			return;
		if ( GameFlow.Screen is not (GameScreen.Pixel or GameScreen.Stage) )
			return;
		Color src = Palettes.Default.Colors[8];
		Rect( _hx, _hy, 1, 1, new Color32( (byte)(src.r * 255f), (byte)(src.g * 255f), (byte)(src.b * 255f), 220 ) );
	}

	void BlitTile( TilesetDoc tiles, int id, int dx, int dy, int zoom )
	{
		if ( tiles?.Sheet is null || id <= 0 )
			return;
		tiles.TileOrigin( id, out var ox, out var oy );
		var ts = tiles.TileSize;
		var sheet = tiles.Sheet;
		for ( var y = 0; y < ts; y++ )
		for ( var x = 0; x < ts; x++ )
		{
			var col = sheet.ColorAt( ox + x, oy + y );
			Color cc = col;
			if ( cc.a < 0.04f )
				continue;
			if ( zoom <= 1 )
			{
				Put( dx + x, dy + y, col );
			}
			else
			{
				FillRect( dx + x * zoom, dy + y * zoom, zoom, zoom, col );
			}
		}
	}

	void Ensure( int w, int h )
	{
		if ( _tex is not null && _tw == w && _th == h )
			return;
		_tw = w;
		_th = h;
		_buf = new Color32[w * h];
		try
		{
			_tex = Texture.Create( w, h )
				.WithName( "pp_crt" )
				.WithDynamicUsage()
				.Finish();
		}
		catch
		{
			_tex = Texture.Create( w, h ).WithName( "pp_crt" ).Finish();
		}
	}

	void Clear( Color32 c )
	{
		for ( var i = 0; i < _buf.Length; i++ )
			_buf[i] = c;
	}

	void Checker( int x, int y, int w, int h )
	{
		var a = new Color32( 22, 28, 48, 255 );
		var b = new Color32( 16, 20, 36, 255 );
		for ( var py = 0; py < h; py++ )
		for ( var px = 0; px < w; px++ )
			Put( x + px, y + py, ((px / 4 + py / 4) & 1) == 0 ? a : b );
	}

	void FillRect( int x, int y, int w, int h, Color32 c )
	{
		for ( var py = 0; py < h; py++ )
		for ( var px = 0; px < w; px++ )
			Put( x + px, y + py, c );
	}

	void Rect( int x, int y, int w, int h, Color32 c )
	{
		HLine( x, y, w, c );
		HLine( x, y + h - 1, w, c );
		VLine( x, y, h, c );
		VLine( x + w - 1, y, h, c );
	}

	void HLine( int x, int y, int w, Color32 c )
	{
		for ( var i = 0; i < w; i++ )
			Put( x + i, y, c );
	}

	void VLine( int x, int y, int h, Color32 c )
	{
		for ( var i = 0; i < h; i++ )
			Put( x, y + i, c );
	}

	void Put( int x, int y, Color32 c )
	{
		if ( (uint)x >= (uint)_tw || (uint)y >= (uint)_th )
			return;
		_buf[y * _tw + x] = c;
	}

	void BlendRect( int x, int y, int w, int h, Color32 src, float a )
	{
		a = Math.Clamp( a, 0f, 1f );
		Color s = src;
		for ( var py = 0; py < h; py++ )
		for ( var px = 0; px < w; px++ )
		{
			var gx = x + px;
			var gy = y + py;
			if ( (uint)gx >= (uint)_tw || (uint)gy >= (uint)_th )
				continue;
			var i = gy * _tw + gx;
			Color d = _buf[i];
			_buf[i] = new Color32(
				(byte)MathF.Round( (d.r * (1f - a) + s.r * a) * 255f ),
				(byte)MathF.Round( (d.g * (1f - a) + s.g * a) * 255f ),
				(byte)MathF.Round( (d.b * (1f - a) + s.b * a) * 255f ),
				255 );
		}
	}
}