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 );
}
}