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

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

	private Pixmap originalPixmap;
	private Pixmap processedPixmap;
	private SeamlessPreviewMode previewMode = SeamlessPreviewMode.Processed;
	private int tileCount = 3;
	private string caption = "Drop an image here or use Open";
	private bool isDragHovering;
	private bool isPanning;
	private bool fitToView = true;
	private float zoom = 1f;
	private Vector2 panOffset;
	private Vector2 lastCursorPosition;

	public SeamlessPreviewWidget( 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 SetBitmaps( Bitmap original, Bitmap processed )
	{
		var originalRecreated = UpdatePixmapFromBitmap( ref originalPixmap, original );
		var processedRecreated = UpdatePixmapFromBitmap( ref processedPixmap, processed );

		if ( originalRecreated || processedRecreated )
			FitToView();
		else
			Update();
	}

	public void SetPreviewMode( SeamlessPreviewMode mode )
	{
		previewMode = mode;
		Update();
	}

	public void SetTileCount( int count )
	{
		tileCount = Math.Clamp( count, 2, 8 );
		Update();
	}

	public void SetCaption( string text )
	{
		caption = text;
		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 = GetDroppedFileList( 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 = GetDroppedFileList( 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 ( GetActivePixmap() == null || !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 ( GetActivePixmap() == null )
			return;

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

	private List<string> GetDroppedFileList( DragData data )
	{
		var files = new List<string>();

		if ( data?.Files != null )
		{
			foreach ( var file in data.Files )
			{
				files.Add( file );
			}
		}

		if ( data?.Assets != null )
		{
			foreach ( var dragAsset in data.Assets )
			{
				if ( !string.IsNullOrWhiteSpace( dragAsset.AssetPath ) )
					files.Add( dragAsset.AssetPath );
			}
		}

		if ( data != null )
		{
			foreach ( var asset in data.OfType<Editor.Asset>() )
			{
				AddAssetPathCandidates( files, asset );
			}
		}

		return files;
	}

	private void AddAssetPathCandidates( List<string> files, Editor.Asset asset )
	{
		if ( asset == null || asset.IsDeleted )
			return;

		if ( !string.IsNullOrWhiteSpace( asset.AbsolutePath ) )
			files.Add( asset.AbsolutePath );

		if ( !string.IsNullOrWhiteSpace( asset.RelativePath ) )
			files.Add( asset.RelativePath );

		if ( !string.IsNullOrWhiteSpace( asset.Path ) )
			files.Add( asset.Path );
	}

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

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

		DrawCheckerboard( rect.Shrink( 16 ) );

		var pixmap = GetActivePixmap();
		if ( pixmap == null )
		{
			DrawEmptyState( rect );
			DrawDropOverlay( rect );
			return;
		}

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

		if ( previewMode == SeamlessPreviewMode.Tiled )
		{
			DrawTiledPreview( imageRect, pixmap );
		}
		else
		{
			DrawSinglePreview( imageRect, pixmap );
		}

		DrawCaption( rect );
		DrawDropOverlay( rect );
	}

	private Pixmap GetActivePixmap()
	{
		return previewMode switch
		{
			SeamlessPreviewMode.Original => originalPixmap,
			SeamlessPreviewMode.Tiled => processedPixmap ?? originalPixmap,
			_ => processedPixmap ?? originalPixmap
		};
	}

	private bool UpdatePixmapFromBitmap( ref Pixmap pixmap, Bitmap bitmap )
	{
		if ( bitmap == null || !bitmap.IsValid )
		{
			var hadPixmap = pixmap != null;
			pixmap = null;
			return hadPixmap;
		}

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

		pixmap = Pixmap.FromBitmap( bitmap );
		return true;
	}

	private void DrawSinglePreview( Rect area, Pixmap pixmap )
	{
		var scale = GetPreviewScale( area, pixmap, 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, Pixmap pixmap )
	{
		var count = Math.Clamp( tileCount, 2, 8 );
		var scale = GetPreviewScale( area, pixmap, 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 float GetPreviewScale( Rect area, Pixmap pixmap, 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 image 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 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 image", TextFlag.Center );
	}
}