Editor/PbrPreviewWidget.cs
using System;
using System.Collections.Generic;
using Editor;
using Sandbox;

public sealed class PbrPreviewWidget : Widget
{
	public Action<string[]> FilesDropped { get; set; }

	private static readonly PbrMapType[] AtlasOrder =
	{
		PbrMapType.Albedo,
		PbrMapType.Height,
		PbrMapType.Normal,
		PbrMapType.Roughness,
		PbrMapType.AmbientOcclusion,
		PbrMapType.Metallic,
		PbrMapType.Orm
	};

	private Pixmap pixmap;
	private readonly Dictionary<PbrMapType, Pixmap> atlasPixmaps = new();
	private PbrPreviewLayout previewLayout = PbrPreviewLayout.Atlas;
	private string caption = "Drop an albedo texture here or use Load Texture";
	private bool isDragHovering;
	private bool isPanning;
	private bool isLoading;
	private bool fitToView = true;
	private float zoom = 1f;
	private Vector2 panOffset;
	private Vector2 lastCursorPosition;
	private DateTime loadingStartedTime;

	public PbrPreviewWidget( Widget parent ) : base( parent )
	{
		AcceptDrops = true;
		MouseTracking = true;
		MinimumSize = new Vector2( 480, 360 );
		SetStyles( "background-color: #111316; border: 1px solid #2d333a; border-radius: 6px;" );
	}

	public void SetBitmap( Bitmap bitmap )
	{
		pixmap = UpdatePixmapFromBitmap( pixmap, bitmap, true );
		Update();
	}

	public void SetResult( PbrGeneratorResult result )
	{
		atlasPixmaps.Clear();

		if ( result != null )
		{
			foreach ( var mapType in AtlasOrder )
			{
				var map = result.GetMap( mapType );
				var mapPixmap = UpdatePixmapFromBitmap( null, map, false );

				if ( mapPixmap != null )
					atlasPixmaps[mapType] = mapPixmap;
			}
		}

		Update();
	}

	public void SetPreviewLayout( PbrPreviewLayout layout )
	{
		previewLayout = layout;
		Update();
	}

	public void SetCaption( string text )
	{
		caption = text;
		Update();
	}

	public void SetLoading( bool loading )
	{
		if ( isLoading == loading )
		{
			if ( isLoading )
				Update();

			return;
		}

		isLoading = loading;
		loadingStartedTime = DateTime.Now;
		Update();
	}

	public void FitToView()
	{
		fitToView = true;
		zoom = 1f;
		panOffset = Vector2.Zero;
		Update();
	}

	public void SetOneToOne()
	{
		fitToView = false;
		zoom = 1f;
		panOffset = Vector2.Zero;
		Update();
	}

	public override void OnDragHover( Widget.DragEvent ev )
	{
		isDragHovering = SeamlessSuiteImageUtility.GetFilesFromDragData( ev.Data ).Count > 0;
		ev.Action = isDragHovering ? DropAction.Copy : DropAction.Ignore;
		Update();
	}

	public override void OnDragLeave()
	{
		isDragHovering = false;
		Update();
	}

	public override void OnDragDrop( Widget.DragEvent ev )
	{
		isDragHovering = false;
		var files = SeamlessSuiteImageUtility.GetFilesFromDragData( ev.Data );

		if ( files.Count > 0 )
		{
			ev.Action = DropAction.Copy;
			FilesDropped?.Invoke( files.ToArray() );
		}
		else
		{
			ev.Action = DropAction.Ignore;
		}

		Update();
	}

	protected override void OnMousePress( MouseEvent e )
	{
		base.OnMousePress( e );

		if ( !HasVisiblePreview() || !e.LeftMouseButton )
			return;

		isPanning = true;
		lastCursorPosition = e.ScreenPosition;
		e.Accepted = true;
	}

	protected override void OnMouseReleased( MouseEvent e )
	{
		base.OnMouseReleased( e );

		if ( e.LeftMouseButton )
		{
			isPanning = false;
			e.Accepted = true;
		}
	}

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

		if ( !isPanning )
			return;

		fitToView = false;
		panOffset += e.ScreenPosition - lastCursorPosition;
		lastCursorPosition = e.ScreenPosition;
		e.Accepted = true;
		Update();
	}

	protected override void OnMouseWheel( WheelEvent e )
	{
		base.OnMouseWheel( e );

		if ( !HasVisiblePreview() )
			return;

		fitToView = false;
		var scale = e.Delta > 0f ? 1.1f : 0.9f;
		zoom = Math.Clamp( zoom * scale, 0.05f, 16f );
		e.Accept();
		Update();
	}

	protected override void OnPaint()
	{
		var rect = LocalRect;

		Paint.ClearPen();
		Paint.SetBrush( Theme.WindowBackground );
		Paint.DrawRect( rect, 6 );

		DrawCheckerboard( rect.Shrink( 16 ) );

		if ( !HasVisiblePreview() )
		{
			DrawEmptyState( rect );
			DrawLoadingOverlay( rect );
			DrawDropOverlay( rect );
			return;
		}

		var imageRect = rect.Shrink( 18 );
		imageRect.Top += 20;

		if ( previewLayout == PbrPreviewLayout.Atlas )
			DrawAtlasPreview( imageRect );
		else if ( previewLayout == PbrPreviewLayout.Single )
			DrawSinglePreview( imageRect );
		else
			DrawTiledPreview( imageRect );

		DrawCaption( rect );
		DrawLoadingOverlay( rect );
		DrawDropOverlay( rect );
	}

	private Pixmap UpdatePixmapFromBitmap( Pixmap currentPixmap, Bitmap bitmap, bool resetViewWhenRecreated )
	{
		if ( bitmap == null || !bitmap.IsValid )
			return null;

		if ( currentPixmap != null && currentPixmap.Width == bitmap.Width && currentPixmap.Height == bitmap.Height )
		{
			if ( currentPixmap.UpdateFromPixels( bitmap ) )
				return currentPixmap;
		}

		if ( resetViewWhenRecreated )
			FitToView();

		return Pixmap.FromBitmap( bitmap );
	}

	private bool HasVisiblePreview()
	{
		if ( previewLayout == PbrPreviewLayout.Atlas )
			return atlasPixmaps.Count > 0;

		return pixmap != null;
	}

	private void DrawSinglePreview( Rect area )
	{
		var scale = GetPreviewScale( area, 1 );
		var width = pixmap.Width * scale;
		var height = pixmap.Height * scale;
		var center = GetPreviewCenter( area );
		var targetRect = new Rect( center.x - width * 0.5f, center.y - height * 0.5f, width, height );

		Paint.BilinearFiltering = true;
		Paint.Draw( targetRect, pixmap, 1f, 3 );

		Paint.SetPen( Theme.BorderLight, 1, PenStyle.Solid );
		Paint.ClearBrush();
		Paint.DrawRect( targetRect, 3 );
	}

	private void DrawTiledPreview( Rect area )
	{
		var count = previewLayout == PbrPreviewLayout.Tiled3X3 ? 3 : 2;
		var scale = GetPreviewScale( area, count );
		var tileWidth = pixmap.Width * scale;
		var tileHeight = pixmap.Height * scale;
		var totalWidth = tileWidth * count;
		var totalHeight = tileHeight * count;
		var center = GetPreviewCenter( area );
		var startX = center.x - totalWidth * 0.5f;
		var startY = center.y - totalHeight * 0.5f;

		Paint.BilinearFiltering = true;

		for ( var y = 0; y < count; y++ )
		{
			for ( var x = 0; x < count; x++ )
			{
				var tileRect = new Rect( startX + x * tileWidth, startY + y * tileHeight, tileWidth + 0.5f, tileHeight + 0.5f );
				Paint.Draw( tileRect, pixmap, 1f, 0 );
			}
		}

		Paint.SetPen( Theme.BorderLight, 1, PenStyle.Solid );
		Paint.ClearBrush();
		Paint.DrawRect( new Rect( startX, startY, totalWidth, totalHeight ), 3 );
	}

	private void DrawAtlasPreview( Rect area )
	{
		var columns = 4;
		var rows = 2;
		var gap = 10f;
		var cellWidth = (area.Width - gap * (columns - 1)) / columns;
		var cellHeight = (area.Height - gap * (rows - 1)) / rows;

		Paint.BilinearFiltering = true;

		for ( var index = 0; index < AtlasOrder.Length; index++ )
		{
			var column = index % columns;
			var row = index / columns;
			var mapType = AtlasOrder[index];
			var cell = new Rect(
				area.Left + column * (cellWidth + gap),
				area.Top + row * (cellHeight + gap),
				cellWidth,
				cellHeight
			);

			DrawAtlasCell( cell, mapType );
		}
	}

	private void DrawAtlasCell( Rect cell, PbrMapType mapType )
	{
		Paint.ClearPen();
		Paint.SetBrush( new Color( 0.07f, 0.078f, 0.086f, 0.92f ) );
		Paint.DrawRect( cell, 4 );

		Paint.SetPen( Theme.BorderLight, 1, PenStyle.Solid );
		Paint.ClearBrush();
		Paint.DrawRect( cell, 4 );

		var labelRect = cell.Shrink( 7 );
		labelRect.Height = 18;
		Paint.SetDefaultFont( 9, 700, false, false );
		Paint.SetPen( Theme.TextLight );
		Paint.DrawText( labelRect, PbrGenerator.GetMapLabel( mapType ), TextFlag.LeftCenter );

		if ( !atlasPixmaps.TryGetValue( mapType, out var mapPixmap ) || mapPixmap == null )
		{
			var missingRect = cell.Shrink( 8 );
			missingRect.Top += 20;
			Paint.SetDefaultFont( 9, 400, false, false );
			Paint.SetPen( Theme.TextDisabled );
			Paint.DrawText( missingRect, "No map", TextFlag.Center );
			return;
		}

		var imageArea = cell.Shrink( 7 );
		imageArea.Top += 20;
		var scaleX = imageArea.Width / Math.Max( 1f, mapPixmap.Width );
		var scaleY = imageArea.Height / Math.Max( 1f, mapPixmap.Height );
		var scale = MathF.Min( scaleX, scaleY );
		var width = mapPixmap.Width * scale;
		var height = mapPixmap.Height * scale;
		var targetRect = new Rect(
			imageArea.Left + (imageArea.Width - width) * 0.5f,
			imageArea.Top + (imageArea.Height - height) * 0.5f,
			width,
			height
		);

		Paint.Draw( targetRect, mapPixmap, 1f, 2 );
	}

	private float GetPreviewScale( Rect area, int tileCount )
	{
		if ( pixmap == null )
			return 1f;

		if ( !fitToView )
			return zoom;

		var widthScale = area.Width / Math.Max( 1f, pixmap.Width * tileCount );
		var heightScale = area.Height / Math.Max( 1f, pixmap.Height * tileCount );
		return Math.Clamp( MathF.Min( widthScale, heightScale ), 0.01f, 1f );
	}

	private Vector2 GetPreviewCenter( Rect area )
	{
		return new Vector2(
			area.Left + area.Width * 0.5f + panOffset.x,
			area.Top + area.Height * 0.5f + panOffset.y
		);
	}

	private void DrawCheckerboard( Rect area )
	{
		var cellSize = 20f;
		var columns = (int)MathF.Ceiling( area.Width / cellSize );
		var rows = (int)MathF.Ceiling( area.Height / cellSize );

		Paint.ClearPen();

		for ( var y = 0; y < rows; y++ )
		{
			for ( var x = 0; x < columns; x++ )
			{
				var color = (x + y) % 2 == 0
					? new Color( 0.145f, 0.155f, 0.165f, 1f )
					: new Color( 0.105f, 0.115f, 0.125f, 1f );

				Paint.SetBrush( color );
				Paint.DrawRect( new Rect( area.Left + x * cellSize, area.Top + y * cellSize, cellSize, cellSize ) );
			}
		}
	}

	private void DrawEmptyState( Rect rect )
	{
		Paint.SetDefaultFont( 14, 500, false, false );
		Paint.SetPen( Theme.TextLight );
		Paint.DrawText( rect, "Drop an albedo texture here", TextFlag.Center );

		var hintRect = rect;
		hintRect.Top += 36;
		Paint.SetDefaultFont( 10, 400, false, false );
		Paint.SetPen( Theme.TextDisabled );
		Paint.DrawText( hintRect, "PNG, JPG, WEBP, BMP, TGA, TIF, PSD, SVG", TextFlag.Center );
	}

	private void DrawCaption( Rect rect )
	{
		var captionRect = rect.Shrink( 14 );
		captionRect.Height = 20;

		Paint.SetDefaultFont( 10, 500, false, false );
		Paint.SetPen( Theme.TextLight );
		Paint.DrawText( captionRect, caption, TextFlag.LeftCenter );
	}

	private void DrawLoadingOverlay( Rect rect )
	{
		if ( !isLoading )
			return;

		var width = Math.Clamp( rect.Width - 32f, 150f, 190f );
		var overlayRect = new Rect( rect.Right - width - 16f, rect.Top + 14f, width, 34f );

		if ( overlayRect.Left < rect.Left + 16f )
			overlayRect.Left = rect.Left + 16f;

		Paint.ClearPen();
		Paint.SetBrush( new Color( 0.05f, 0.06f, 0.07f, 0.88f ) );
		Paint.DrawRect( overlayRect, 5 );

		DrawLoadingSpinner( new Vector2( overlayRect.Left + 18f, overlayRect.Center.y ) );

		var elapsed = (DateTime.Now - loadingStartedTime).TotalSeconds;
		var dotCount = (int)(elapsed * 3.5) % 4;
		var text = $"Building maps{new string( '.', dotCount )}";
		var textRect = overlayRect;
		textRect.Left += 36f;
		textRect.Right -= 10f;

		Paint.SetDefaultFont( 10, 600, false, false );
		Paint.SetPen( Theme.TextLight );
		Paint.DrawText( textRect, text, TextFlag.LeftCenter );
	}

	private void DrawLoadingSpinner( Vector2 center )
	{
		var elapsed = (float)(DateTime.Now - loadingStartedTime).TotalSeconds;
		var activeDot = (int)(elapsed * 10f) % 8;

		for ( var i = 0; i < 8; i++ )
		{
			var angle = i / 8f * MathF.PI * 2f;
			var position = center + new Vector2( MathF.Cos( angle ), MathF.Sin( angle ) ) * 8f;
			var distance = (i - activeDot + 8) % 8;
			var alpha = 0.25f + (7 - distance) / 7f * 0.55f;

			Paint.ClearPen();
			Paint.SetBrush( Theme.Blue.WithAlpha( alpha ) );
			Paint.DrawCircle( new Rect( position.x - 2f, position.y - 2f, 4f, 4f ) );
		}
	}

	private void DrawDropOverlay( Rect rect )
	{
		if ( !isDragHovering )
			return;

		Paint.ClearPen();
		Paint.SetBrush( Theme.Blue.WithAlpha( 0.22f ) );
		Paint.DrawRect( rect.Shrink( 4 ), 6 );

		Paint.SetPen( Theme.TextSelected );
		Paint.SetDefaultFont( 16, 700, false, false );
		Paint.DrawText( rect, "Release to load texture", TextFlag.Center );
	}
}